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

풀이

조건

  1. Primitive 타입의 경우 그 자체로 출력하거나, Union 값을 출력할 수 있어야 한다.
  1. Array 타입의 경우 item이 지정된 경우 그 타입을 출력하거나 없으면 unknown[]로 출력한다.
  1. Object 타입의 경우 Property에 맞게 출력하거나, Object 타입으로 출력한다.
  1. 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