FE/type-challenge
26401. JSONSchema2TS
최토피
2023. 10. 18. 12:21
728x90
문제
Implement the generic type JSONSchema2TS which will return the TypeScript type corresponding to the given JSON schema.
cases
// + Primitive types
type Type1 = JSONSchema2TS<{
type: 'string'
}>
type Expected1 = string
type Result1 = Expect<Equal<Type1, Expected1>>
type Type2 = JSONSchema2TS<{
type: 'number'
}>
type Expected2 = number
type Result2 = Expect<Equal<Type2, Expected2>>
type Type3 = JSONSchema2TS<{
type: 'boolean'
}>
type Expected3 = boolean
type Result3 = Expect<Equal<Type3, Expected3>>
// - Primitive types
// + Enums
type Type4 = JSONSchema2TS<{
type: 'string'
enum: ['a', 'b', 'c']
}>
type Expected4 = 'a' | 'b' | 'c'
type Result4 = Expect<Equal<Type4, Expected4>>
type Type5 = JSONSchema2TS<{
type: 'number'
enum: [1, 2, 3]
}>
type Expected5 = 1 | 2 | 3
type Result5 = Expect<Equal<Type5, Expected5>>
// - Enums
// + Object types
type Type6 = JSONSchema2TS<{
type: 'object'
}>
type Expected6 = Record<string, unknown>
type Result6 = Expect<Equal<Type6, Expected6>>
type Type7 = JSONSchema2TS<{
type: 'object'
properties: {}
}>
type Expected7 = {}
type Result7 = Expect<Equal<Type7, Expected7>>
type Type8 = JSONSchema2TS<{
type: 'object'
properties: {
a: {
type: 'string'
}
}
}>
type Expected8 = {
a?: string
}
type Result8 = Expect<Equal<Type8, Expected8>>
// - Object types
// + Arrays
type Type9 = JSONSchema2TS<{
type: 'array'
}>
type Expected9 = unknown[]
type Result9 = Expect<Equal<Type9, Expected9>>
type Type10 = JSONSchema2TS<{
type: 'array'
items: {
type: 'string'
}
}>
type Expected10 = string[]
type Result10 = Expect<Equal<Type10, Expected10>>
type Type11 = JSONSchema2TS<{
type: 'array'
items: {
type: 'object'
}
}>
type Expected11 = Record<string, unknown>[]
type Result11 = Expect<Equal<Type11, Expected11>>
// - Arrays
// + Mixed types
type Type12 = JSONSchema2TS<{
type: 'object'
properties: {
a: {
type: 'string'
enum: ['a', 'b', 'c']
}
b: {
type: 'number'
}
}
}>
type Expected12 = {
a?: 'a' | 'b' | 'c'
b?: number
}
type Result12 = Expect<Equal<Type12, Expected12>>
type Type13 = JSONSchema2TS<{
type: 'array'
items: {
type: 'object'
properties: {
a: {
type: 'string'
}
}
}
}>
type Expected13 = {
a?: string
}[]
type Result13 = Expect<Equal<Type13, Expected13>>
// - Mixed types
// + Required fields
type Type14 = JSONSchema2TS<{
type: 'object'
properties: {
req1: { type: 'string' }
req2: {
type: 'object'
properties: {
a: {
type: 'number'
}
}
required: ['a']
}
add1: { type: 'string' }
add2: {
type: 'array'
items: {
type: 'number'
}
}
}
required: ['req1', 'req2']
}>
type Expected14 = {
req1: string
req2: { a: number }
add1?: string
add2?: number[]
}
type Result14 = Expect<Equal<Type14, Expected14>>
// - Required fields
정답
type JSONType = 'string' | 'number' | 'boolean' | 'array' | 'object'
type JSONMap = {
string: string
number: number
boolean: boolean
}
type JSONSchema = {
type: JSONType
enum?: unknown[]
items?: JSONSchema
properties?: Record<string, JSONSchema>
required?: string[]
}
type CombineObjects<T extends object, K extends object> = {
[key in keyof (T & K)]: (T & K)[key]
}
type RequiredTS<T extends object, K extends (keyof T)> =
CombineObjects<
{ [key in keyof T as key extends K ? key : never]-?: T[key] },
{ [key in keyof T as key extends K ? never : key]: T[key] }>
type JSONSchema2TS<T extends JSONSchema> =
T['type'] extends 'object'
? T['properties'] extends object
? T['required'] extends string[]
? RequiredTS<{ [key in keyof T['properties']]?: JSONSchema2TS<T['properties'][key]> }, T['required'][number]>
: { [key in keyof T['properties']]?: JSONSchema2TS<T['properties'][key]> }
: Record<string, unknown>
: T['type'] extends 'array'
? T['items'] extends JSONSchema
? JSONSchema2TS<T['items']>[]
: unknown[]
: T['type'] extends 'string' | 'number' | 'boolean'
? T['enum'] extends unknown[]
? T['enum'][number]
: JSONMap[T['type']]
: unknown
풀이
조건
- Primitive 타입의 경우 그 자체로 출력하거나, Union 값을 출력할 수 있어야 한다.
- Array 타입의 경우 item이 지정된 경우 그 타입을 출력하거나 없으면 unknown[]로 출력한다.
- Object 타입의 경우 Property에 맞게 출력하거나, Object 타입으로 출력한다.
- Object 타입의 경우 기본적으로 key가 optional이어야 하지만, required를 통해 non-optional으로 지정할 수 있어야 한다.
해설
조건 1에 따라 Primitive의 경우를 먼저 구현한다.
type JSONType = 'string' | 'number' | 'boolean' | 'array' | 'object'
type JSONMap = {
string: string
number: number
boolean: boolean
}
type JSONSchema = {
type: JSONType
enum?: unknown[]
}
type JSONSchema2TS<T extends JSONSchema> =
T['type'] extends 'string' | 'number' | 'boolean'
? T['enum'] extends unknown[]
? T['enum'][number]
: JSONMap[T['type']]
: unknown
위의 코드에 대한 설명은 다음과 같다.
JSONType
: 입력 가능한 Type
JSONMap
: Primitive를 출력하기 위한 Map
JSONSchema
: JSONSchema2TS의 입력 파라미터
JSONSchema2TS는 입력값에 enum이 있으면 그 타입을 반환하고, 아니면 Map에서 해당 Primitive를 가져와서 반환한다.
조건 2에 따라 Array의 경우를 구현한다.
type JSONSchema = {
type: JSONType
enum?: unknown[]
items?: JSONSchema
}
type JSONSchema2TS<T extends JSONSchema> =
T['type'] extends 'array'
? T['items'] extends JSONSchema
? JSONSchema2TS<T['items']>[]
: unknown[]
: T['type'] extends 'string' | 'number' | 'boolean'
? T['enum'] extends unknown[]
? T['enum'][number]
: JSONMap[T['type']]
: unknown
items
가 있는 경우 재귀적으로 처리한 값을 반환하고, 없으면 unknown[]
을 반환한다.
조건 3에 따라 Object의 경우는 다음과 같이 구현한다.
type JSONSchema = {
type: JSONType
enum?: unknown[]
properties?: Record<string, JSONSchema>
items?: JSONSchema
}
type JSONSchema2TS<T extends JSONSchema> =
T['type'] extends 'object'
? T['properties'] extends object
? { [key in keyof T['properties']]?: JSONSchema2TS<T['properties'][key]> }
: Record<string, unknown>
properties
가 있는 경우 재귀적으로 처리한 값을 반환하고, 없으면 일반적인 object 타입인 Record<string, unknown>
을 반환한다.
조건 4에 따라 required를 추가한다.
type CombineObjects<T extends object, K extends object> = {
[key in keyof (T & K)]: (T & K)[key]
}
type RequiredTS<T extends object, K extends (keyof T)> =
CombineObjects<
{ [key in keyof T as key extends K ? key : never]-?: T[key] },
{ [key in keyof T as key extends K ? never : key]: T[key] }>
type JSONSchema2TS<T extends JSONSchema> =
T['type'] extends 'object'
? T['properties'] extends object
? T['required'] extends string[]
? RequiredTS<{ [key in keyof T['properties']]?: JSONSchema2TS<T['properties'][key]> }, T['required'][number]>
: { [key in keyof T['properties']]?: JSONSchema2TS<T['properties'][key]> }
: Record<string, unknown>
추가된 타입은 다음과 같다
- RequiredTS: optional한 key를 required로 바뀌준다.
- CombineObjects: 두 object를 union이 아닌 하나의 타입으로 합친다.
JSONSchema2TS에 추가된 로직은 required가 있는 경우, RequiredTS 처리하는 로직을 추가한다.
혹시 오류나 개선점이 있으면 댓글 부탁드립니다.
728x90