Skip to content

Commit

Permalink
Merge pull request #31 from nemuvski/new-feature-utils
Browse files Browse the repository at this point in the history
関数と型ユーティリティを追加, 一部関数のドキュメント追記
  • Loading branch information
nemuvski authored Feb 29, 2024
2 parents 0403a1d + 52ae4af commit 269aabb
Show file tree
Hide file tree
Showing 22 changed files with 494 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/utils/src/functions/getHashFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isString } from './isString'
*
* @param url
* @returns {string}
* @throws {URIError} 内部で利用している `decodeURIComponent()` がエラーをスローする可能性がある
* @example
* // 返値: hash fragment
* getHashFragment('https://localhost:8080?test=32#hash+fragment')
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/functions/getQueryString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { removeHashFragment } from './removeHashFragment'
*
* @param url
* @returns {string}
* @throws {URIError} 内部で利用している `decodeURIComponent()` がエラーをスローする可能性がある
* @example
* // 返値: ?test1=32&test2=ア
* getQueryString('https://localhost:8080?test1=32&test2=%E3%82%A2#fragment')
Expand Down
28 changes: 28 additions & 0 deletions packages/utils/src/functions/safeDecodeURIComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* decodeURIComponent()で発生する例外をキャッチして、例外が発生した場合はエラーオブジェクトを返却する
*
* @param value
* @returns {{ data: string; error: null } | { data: null; error: URIError }}
* @example
* const { data, error } = safeDecodeURIComponent('foo%20bar') // => { data: 'foo bar', error: null }
* if (error) {
* console.error(error)
* } else {
* console.log(data) // => 'foo bar'
* }
*/
export function safeDecodeURIComponent(value: string): { data: string; error: null } | { data: null; error: URIError } {
try {
const data = decodeURIComponent(value)
return { data, error: null }
} catch (e) {
return {
data: null,
/**
* try中で発生する例外はdecodeURIComponentの仕様によるものなので、SyntaxErrorでアサーションしている
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#exceptions}
*/
error: e as URIError,
}
}
}
28 changes: 28 additions & 0 deletions packages/utils/src/functions/safeEncodeURIComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* encodeURIComponent()で発生する例外をキャッチして、例外が発生した場合はエラーオブジェクトを返却する
*
* @param value
* @returns {{ data: string; error: null } | { data: null; error: URIError }}
* @example
* const { data, error } = safeEncodeURIComponent('foo bar') // => { data: 'foo%20bar', error: null }
* if (error) {
* console.error(error)
* } else {
* console.log(data) // => 'foo%20bar'
* }
*/
export function safeEncodeURIComponent(value: string): { data: string; error: null } | { data: null; error: URIError } {
try {
const data = encodeURIComponent(value)
return { data, error: null }
} catch (e) {
return {
data: null,
/**
* try中で発生する例外はencodeURIComponentの仕様によるものなので、SyntaxErrorでアサーションしている
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#exceptions}
*/
error: e as SyntaxError,
}
}
}
36 changes: 36 additions & 0 deletions packages/utils/src/functions/safeJsonParse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* JSON.parse()で発生する例外をキャッチして、例外が発生した場合はエラーオブジェクトを返す
*
* 型変数 `T` はJSON.parse()でパースするデータの型を指定する
*
* ※ ただし、型ガードは行わないため、呼び出し元で型ガードを適宜実施すること
*
* @param value
* @param options
* @returns {{ data: T; error: null } | { data: null; error: SyntaxError }}
* @example
* const { data, error } = safeJsonParse<{ foo: string }>('{"foo": "bar"}') // => { data: { foo: 'bar' }, error: null }
* if (error) {
* console.error(error)
* } else {
* console.log(data) // => { foo: 'bar' }
* }
*/
export function safeJsonParse<T>(
value: string,
options?: { reviver?: Parameters<typeof JSON.parse>[1] }
): { data: T; error: null } | { data: null; error: SyntaxError } {
try {
const data = JSON.parse(value, options?.reviver)
return { data, error: null }
} catch (e) {
return {
data: null,
/**
* try中で発生する例外はJSON.parseの仕様によるものなので、SyntaxErrorでアサーションしている
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#exceptions}
*/
error: e as SyntaxError,
}
}
}
35 changes: 35 additions & 0 deletions packages/utils/src/functions/safeJsonStringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* JSON.stringify()で発生する例外をキャッチして、例外が発生した場合はエラーオブジェクトを返す
*
* @param value
* @param options
* @returns {{ data: string; error: null } | { data: null; error: TypeError }}
* @example
* const { data, error } = safeJsonStringify({ foo: 'bar' }) // => { data: '{"foo":"bar"}', error: null }
* if (error) {
* console.error(error)
* } else {
* console.log(data) // => '{"foo":"bar"}'
* }
*/
export function safeJsonStringify<T>(
value: T,
options?: {
replacer?: Parameters<typeof JSON.stringify>[1]
space?: Parameters<typeof JSON.stringify>[2]
}
): { data: string; error: null } | { data: null; error: TypeError } {
try {
const data = JSON.stringify(value, options?.replacer, options?.space)
return { data, error: null }
} catch (e) {
return {
data: null,
/**
* try中で発生する例外はJSON.stringifyの仕様によるものなので、TypeErrorでアサーションしている
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions}
*/
error: e as TypeError,
}
}
}
4 changes: 4 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export * from './functions/replaceHwAlphanumericsWithFw'
export * from './functions/replaceNewLineChars'
export * from './functions/replaceSpacesWithTab'
export * from './functions/replaceTabWithSpaces'
export * from './functions/safeDecodeURIComponent'
export * from './functions/safeEncodeURIComponent'
export * from './functions/safeJsonParse'
export * from './functions/safeJsonStringify'
export * from './functions/separateArray'
export * from './functions/strf'
export * from './functions/withRootRelativePath'
Expand Down
18 changes: 18 additions & 0 deletions packages/utils/src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ export type ExactNotMatchTypeKeys<T, U> = keyof Omit<T, ExactMatchTypeKeys<T, U>
export type RequiredAtLeastOne<T, K extends keyof T = keyof T> = Pick<T, Exclude<keyof T, K>> &
Partial<Pick<T, K>> &
(K extends keyof T ? { [_K in K]-?: Pick<Required<T>, _K> }[K] : never)

/**
* オブジェクトの全フィールドの型からundefined,nullを除外した型を得る
*
* ※ ネストしたオブジェクトのフィールドに対しては適用されない
*
* @example
* type Post = {
* id: `post_${number}`
* title: string
* body: string | undefined
* author: { name: string; age: number | undefined }
* createdAt: Date
* updatedAt?: Date | null
* }
* type NewPost = NonNullishFields<Post> // { id: `post_${number}`; title: string; body: string; author: { name: string; age: number | undefined }; createdAt: Date; updatedAt: Date }
*/
export type NonNullishFields<T extends {}> = { [K in keyof T]-?: NonNullable<T[K]> }
3 changes: 2 additions & 1 deletion packages/utils/tests/functions/getHashFragment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest'
import { expect, test } from 'vitest'
import { getHashFragment } from '../../src'

test('getHashFragment()', () => {
Expand All @@ -8,4 +8,5 @@ test('getHashFragment()', () => {
expect(getHashFragment('https://localhost:8080?test=32#hash_fragment')).toBe('hash_fragment')
expect(getHashFragment('https://localhost:8080?test=32#hash+fragment')).toBe('hash fragment')
expect(getHashFragment('https://localhost:8080?test=32#%E3%83%86%E3%82%B9%E3%83%88')).toBe('テスト')
expect(() => getHashFragment('https://localhost:8080?test=32#%E0%A4%A')).toThrowError(URIError)
})
3 changes: 2 additions & 1 deletion packages/utils/tests/functions/getQueryString.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest'
import { expect, test } from 'vitest'
import { getQueryString } from '../../src'

test('getQueryString()', () => {
Expand All @@ -8,4 +8,5 @@ test('getQueryString()', () => {
expect(getQueryString('https://localhost:8080?#test1=3+2&test2=%E3%82%A2')).toBe('?')
expect(getQueryString('https://localhost:8080#?test1=3+2&test2=%E3%82%A2')).toBe('')
expect(getQueryString('https://localhost:8080/#?test1=32&test2=%E3%82%A2')).toBe('')
expect(() => getQueryString('https://localhost:8080/?test1=32&test2=%E0%A4%A')).toThrowError(URIError)
})
16 changes: 16 additions & 0 deletions packages/utils/tests/functions/safeDecodeURIComponent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from 'vitest'
import { safeDecodeURIComponent } from '../../src'

describe('safeDecodeURIComponent()', () => {
test('デコードされた値が返ってくる', () => {
expect(safeDecodeURIComponent('%3Fx%3Dtest')).toEqual({ data: '?x=test', error: null })
expect(safeDecodeURIComponent('foo%20bar')).toEqual({ data: 'foo bar', error: null })
expect(safeDecodeURIComponent('%E3%83%86%E3%82%B9%E3%83%88')).toEqual({ data: 'テスト', error: null })
})

test('URIErrorが返ってくる', () => {
const { data, error } = safeDecodeURIComponent('%')
expect(data).toBeNull()
expect(error).toBeInstanceOf(URIError)
})
})
16 changes: 16 additions & 0 deletions packages/utils/tests/functions/safeEncodeURIComponent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from 'vitest'
import { safeEncodeURIComponent } from '../../src'

describe('safeEncodeURIComponent()', () => {
test('エンコードされた値が返ってくる', () => {
expect(safeEncodeURIComponent('?x=test')).toEqual({ data: '%3Fx%3Dtest', error: null })
expect(safeEncodeURIComponent('foo bar')).toEqual({ data: 'foo%20bar', error: null })
expect(safeEncodeURIComponent('テスト')).toEqual({ data: '%E3%83%86%E3%82%B9%E3%83%88', error: null })
})

test('URIErrorが返ってくる', () => {
const { data, error } = safeEncodeURIComponent('\uD800')
expect(data).toBeNull()
expect(error).toBeInstanceOf(URIError)
})
})
34 changes: 34 additions & 0 deletions packages/utils/tests/functions/safeJsonParse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from 'vitest'
import { safeJsonParse } from '../../src'

describe('safeJsonParse()', () => {
describe('パースに成功する', () => {
test('optionsなし', () => {
const jsonString = '{"foo": "bar"}'
const { data, error } = safeJsonParse<{ foo: string }>(jsonString)
expect(data).toEqual({ foo: 'bar' })
expect(error).toBeNull()
})

test('options.reviverあり', () => {
const jsonString = '{"foo": "bar"}'
const { data, error } = safeJsonParse<{ foo: string }>(jsonString, {
reviver: (key, value) => {
if (key === 'foo') {
return `${value}!`
}
return value
},
})
expect(data).toEqual({ foo: 'bar!' })
expect(error).toBeNull()
})
})

test('パースに失敗する', () => {
const jsonString = '{"foo": "bar"'
const { data, error } = safeJsonParse(jsonString)
expect(data).toBeNull()
expect(error).toBeInstanceOf(SyntaxError)
})
})
65 changes: 65 additions & 0 deletions packages/utils/tests/functions/safeJsonStringify.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, test } from 'vitest'
import { safeJsonStringify } from '../../src'

describe('safeJsonStringify()', () => {
describe('文字列化に成功する', () => {
test('optionsなし', () => {
const { data, error } = safeJsonStringify({ foo: 'bar' })
expect(data).toBe('{"foo":"bar"}')
expect(error).toBeNull()
})

test('options.replacerあり (関数のケース)', () => {
const target = { foo: 'bar', hoo: 3, goo: true }
const { data, error } = safeJsonStringify(target, {
// @ts-ignore: 問題ないので型エラーを無視
replacer: (key: keyof typeof target, value: (typeof target)[keyof typeof target]) => {
if (key === 'foo') {
return `${value}!`
}
if (key === 'hoo' && typeof value === 'number') {
return value * 2
}
return value
},
})
expect(data).toBe('{"foo":"bar!","hoo":6,"goo":true}')
expect(error).toBeNull()
})

test('options.replacerあり (配列のケース)', () => {
const target = { foo: 'bar', hoo: 3, goo: true, 3: 'three' }
const { data, error } = safeJsonStringify(target, {
replacer: ['foo', 'hoo', 3],
})
expect(data).toBe('{"foo":"bar","hoo":3,"3":"three"}')
expect(error).toBeNull()
})

test('options.spaceあり (数値のケース)', () => {
const target = { foo: 'bar' }
const { data, error } = safeJsonStringify(target, {
space: 2,
})
expect(data).toBe('{\n "foo": "bar"\n}')
expect(error).toBeNull()
})

test('options.spaceあり (文字のケース)', () => {
const target = { foo: 'bar' }
const { data, error } = safeJsonStringify(target, {
space: '\t',
})
expect(data).toBe('{\n\t"foo": "bar"\n}')
expect(error).toBeNull()
})
})

test('文字列化に失敗する', () => {
// BigInt は JSON.stringify で文字列化できないので例外が発生する
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions
const { data, error } = safeJsonStringify({ bigint: 2n })
expect(data).toBeNull()
expect(error).toBeInstanceOf(TypeError)
})
})
29 changes: 26 additions & 3 deletions packages/utils/tests/types/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { expectTypeOf, describe, test } from 'vitest'
import { describe, expectTypeOf, test } from 'vitest'
import type {
MatchTypeKeys,
ExactMatchTypeKeys,
NotMatchTypeKeys,
ExactNotMatchTypeKeys,
MatchTypeKeys,
NonNullishFields,
NotMatchTypeKeys,
RequiredAtLeastOne,
} from '../../src'

Expand Down Expand Up @@ -113,4 +114,26 @@ describe('types/utils.ts', () => {
expectTypeOf({}).not.toEqualTypeOf<Test>()
})
})

test('NonNullishFields', () => {
expectTypeOf<
NonNullishFields<{
id: `post_${number}`
title: string
body: string | undefined
author: { name: string; age: number | undefined }
createdAt: Date
updatedAt?: Date | null
}>
>().toEqualTypeOf<{
id: `post_${number}`
title: string
body: string
author: { name: string; age: number | undefined }
createdAt: Date
updatedAt: Date
}>()

expectTypeOf<NonNullishFields<string>>().toEqualTypeOf<string>()
})
})
Loading

0 comments on commit 269aabb

Please sign in to comment.