🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Type-challenges] 3, 8, 9, 10, 12
    Front-End/TypeScript 2024. 3. 2. 09:51
    🍀 목차
    3 - Omit
    8 - Readonly 2
    9 - Deep Readonly
    10 - Tuple to Union
    12 - Chainable Options

     

     

    3 - Omit

    // T에서 K 프로퍼티만 제거해 새로운 오브젝트 타입을 만드는 
    // 내장 제네릭 Omit<T, K>를 이를 사용하지 않고 구현하세요.
    
    // 예시
    interface Todo {
      title: string
      description: string
      completed: boolean
    }
    
    type TodoPreview = MyOmit<Todo, 'description' | 'title'>
    
    const todo: TodoPreview = {
      completed: false,
    }

     

    /* _____________ 여기에 코드 입력 _____________ */
    
    type MyOmit<T, K> = any
    
    /* _____________ 테스트 케이스 _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,
      Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>,
    ]
    
    // @ts-expect-error
    type error = MyOmit<Todo, 'description' | 'invalid'>
    
    interface Todo {
      title: string
      description: string
      completed: boolean
    }
    
    interface Expected1 {
      title: string
      completed: boolean
    }
    
    interface Expected2 {
      title: string
    }

     

    •  Exclude를 사용한 풀이
    type MyOmit<T, K extends keyof T> = {
      [key in Exclude<keyof T, K>]: T[key];
    }

     

    Exclude<T, U>T extends U ? never : T이며, T에오는 U를 제외하겠다는 뜻이 된다.

     

    • Exclude를 사용하지 않은 풀이
    type MyOmit<T, K extends keyof T> ={
      [key in keyof T as key extends K ? never : key] : T[key];
    }

     

     Mapped Types와 Conditional Types를 사용한다. TypeScript 4.1 이상에서는 as를 통해 매핑된 타입의 키를 다시 매핑할 수 있다.

     

    8 - Readonly 2

    // T에서 K 프로퍼티만 읽기 전용으로 설정해 
    // 새로운 오브젝트 타입을 만드는 제네릭 MyReadonly2<T, K>를 구현하세요. 
    // K가 주어지지 않으면 단순히 Readonly<T>처럼 모든 프로퍼티를 읽기 전용으로 설정해야 합니다.
    
    // 예시
    interface Todo {
      title: string
      description: string
      completed: boolean
    }
    
    const todo: MyReadonly2<Todo, 'title' | 'description'> = {
      title: "Hey",
      description: "foobar",
      completed: false,
    }
    
    todo.title = "Hello" // Error: cannot reassign a readonly property
    todo.description = "barFoo" // Error: cannot reassign a readonly property
    todo.completed = true // OK

     

    /* _____________ 여기에 코드 입력 _____________ */
    
    type MyReadonly2<T, K> = any
    
    /* _____________ 테스트 케이스 _____________ */
    import type { Alike, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
      Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
      Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
      Expect<Alike<MyReadonly2<Todo2, 'description' >, Expected>>,
    ]
    
    // @ts-expect-error
    type error = MyReadonly2<Todo1, 'title' | 'invalid'>
    
    interface Todo1 {
      title: string
      description?: string
      completed: boolean
    }
    
    interface Todo2 {
      readonly title: string
      description?: string
      completed: boolean
    }
    
    interface Expected {
      readonly title: string
      readonly description?: string
      completed: boolean
    }

     

     

     K가 주어지지 않으면 모든 프로퍼티를 읽기 전용으로 설정해야 하는 점을 유의해야 한다. 이 부분에서 막혀서 정답을 보고 공부하였다.

    type MyReadonly2<T, K extends keyof T = keyof T> = {
      [key in keyof T as key extends K? never : key] : T[key]
    } & {
      readonly [key in K] : T[key]
    }

     

     우선 바로 위(3번) 문제였던 MyOmit을 재사용(유틸리티 타입 Omit이나 Pick을 사용해도 된다)하여 key가 K에 해당하는 프로퍼티라면 제외시킨다. 그렇지 않으면 key로 해당 프로퍼티를 포함시킨다.

    이렇게 하면 T에서 K 프로퍼티만 제외하여 새로운 오브젝트가 만들어지는데, 이를 readonly [key in K] : T[key]&(Intersection)한다. 그렇게 K 프로퍼티만 readonly 시킨 결과가 나온다. 

     

     K가 없을 때를 위해 K를 추가로 keyof T로 설정한다. K가 명시되지 않으면 default parameter로 T의 프로퍼티를 주는 것이다.

     

     


    9 - Deep Readonly

    // 객체의 프로퍼티와 모든 하위 객체를 재귀적으로 읽기 전용으로 설정하는 
    // 제네릭 DeepReadonly<T>를 구현하세요.
    
    // * 이 챌린지에서는 타입 파라미터 T를 객체 타입으로 제한하고 있습니다. 
    // 객체뿐만 아니라 배열, 함수, 클래스 등 가능한 다양한 형태의 타입 파라미터를 사용하도록 도전해 보세요.
    
    type X = { 
      x: { 
        a: 1
        b: 'hi'
      }
      y: 'hey'
    }
    
    type Expected = { 
      readonly x: { 
        readonly a: 1
        readonly b: 'hi'
      }
      readonly y: 'hey' 
    }
    
    type Todo = DeepReadonly<X> // should be same as `Expected`

     

    /* _____________ 여기에 코드 입력 _____________ */
    
    type DeepReadonly<T> = any
    
    /* _____________ 테스트 케이스 _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<DeepReadonly<X1>, Expected1>>,
      Expect<Equal<DeepReadonly<X2>, Expected2>>,
    ]
    
    type X1 = {
      a: () => 22
      b: string
      c: {
        d: boolean
        e: {
          g: {
            h: {
              i: true
              j: 'string'
            }
            k: 'hello'
          }
          l: [
            'hi',
            {
              m: ['hey']
            },
          ]
        }
      }
    }
    
    type X2 = { a: string } | { b: number }
    
    type Expected1 = {
      readonly a: () => 22
      readonly b: string
      readonly c: {
        readonly d: boolean
        readonly e: {
          readonly g: {
            readonly h: {
              readonly i: true
              readonly j: 'string'
            }
            readonly k: 'hello'
          }
          readonly l: readonly [
            'hi',
            {
              readonly m: readonly ['hey']
            },
          ]
        }
      }
    }
    
    type Expected2 = { readonly a: string } | { readonly b: number }

     

    type DeepReadonly<T> = {
      readonly [key in keyof T] : keyof T[key] extends never ? T[key] : DeepReadonly<T[key]>
    }

     

      T 객체의 모든 키를 반복하며 해당 키(key)에 대한 keyof T[key]를 검색하여(T[key]의 key를 검사) 없으면 해당 값을 반환하여 readonly로, 있다면 재귀적으로 다시 해당 key에 대해 DeepReadonly로 들어간다. 


    10 - Tuple to Union

    // 튜플 값으로 유니온 타입을 생성하는 제네릭 TupleToUnion<T>를 구현하세요.
    
    // 예시
    type Arr = ['1', '2', '3']
    
    type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

     

    /* _____________ 여기에 코드 입력 _____________ */
    
    type TupleToUnion<T> = any
    
    /* _____________ 테스트 케이스 _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
      Expect<Equal<TupleToUnion<[123]>, 123>>,
    ]

     

    type TupleToUnion<T extends readonly any[]> = T[number];

     


    12 - Chainable Options

    // 체인 가능 옵션은 일반적으로 Javascript에서 사용됩니다. 
    // 하지만 TypeScript로 전환하면 제대로 구현할 수 있나요?
    
    // 이 챌린지에서는 option(key, value)과 get() 두가지 함수를 제공하는 
    // 객체(또는 클래스) 타입을 구현해야 합니다. 
    // 현재 타입을 option으로 지정된 키와 값으로 확장할 수 있고 
    // get으로 최종 결과를 가져올 수 있어야 합니다.
    
    
    // 예시
    declare const config: Chainable
    
    const result = config
      .option('foo', 123)
      .option('name', 'type-challenges')
      .option('bar', { value: 'Hello World' })
      .get()
    
    // 결과는 다음과 같습니다:
    interface Result {
      foo: number
      name: string
      bar: {
        value: string
      }
    }
    
    // 문제를 해결하기 위해 js/ts 로직을 작성할 필요는 없습니다. 단지 타입 수준입니다.
    
    // key는 string만 허용하고 value는 무엇이든 될 수 있다고 가정합니다. 
    // 같은 key는 두 번 전달되지 않습니다.

     

    /* _____________ 여기에 코드 입력 _____________ */
    
    type Chainable = {
      option(key: string, value: any): any
      get(): any
    }
    
    /* _____________ 테스트 케이스 _____________ */
    import type { Alike, Expect } from '@type-challenges/utils'
    
    declare const a: Chainable
    
    const result1 = a
      .option('foo', 123)
      .option('bar', { value: 'Hello World' })
      .option('name', 'type-challenges')
      .get()
    
    const result2 = a
      .option('name', 'another name')
      // @ts-expect-error
      .option('name', 'last name')
      .get()
    
    const result3 = a
      .option('name', 'another name')
      // @ts-expect-error
      .option('name', 123)
      .get()
    
    type cases = [
      Expect<Alike<typeof result1, Expected1>>,
      Expect<Alike<typeof result2, Expected2>>,
      Expect<Alike<typeof result3, Expected3>>,
    ]
    
    type Expected1 = {
      foo: number
      bar: {
        value: string
      }
      name: string
    }
    
    type Expected2 = {
      name: string
    }
    
    type Expected3 = {
      name: number
    }

     

    Chainable<T> 타입은 option(key, value),  get() 두 함수를 갖고 있다.

    • option(key, value) : key, value를 입력받아 객체를 확장한다.
      • key는 string만, value는 무엇이든 올 수 있다.
      • 같은 key는 후에 선언된 value로 갱신된다.
    • get() : 현재까지 만들어진 객체를 반환한다. 

     

    처음 작성한 답은 아래와 같다.

    // 유틸리티 타입, 구현하지 않고 Record<K,V>를 사용해도 됨
    type Record<K extends string, V> = {
      [key in K] : V;
    }
    
    type Chainable<T = {}> = {
      option: <K extends string, V>(key: K, value: V) => Chainable<T & Record<K,V>>
      get: () => T
    }

     

     

     이 경우 result3이 Expected3과 달리 교차되며 string | number로 추론된다.

    type Expected3 = {
      name: number
    }

     

     그렇기 때문에 기존 존재하는 (key, value)를 제거시키고 신규로 입력된 (key, value)로 갱신시키는 부분이 필요하다. 

    해당부분은 Omit을 사용하면 된다. 

    // Omit과 Record는 구현하지 않아도 된다.
    type Omit<T, K> = {
      [key in keyof T as key extends K? never: key] : T[key];
    }
    
    type Record<K extends string, V> = {
      [key in K] : V;
    }
    
    type Chainable<T = {}> = {
      option:<K extends string, V>(key:K, value:V) => Chainable<Omit<T,K> & Record<K,V>>
      get: () => T
    }

     

     

    참고자료

    https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.ko.md

    https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as

    https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.ko.md

    https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ko.md

    https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.ko.md

    https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.ko.md

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼