ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Type-challenges] 난이도 Easy - (1)
    Front-End(Web)/Typescript 2023. 1. 24. 18:21
    반응형

    Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)

     

    https://github.com/type-challenges/type-challenges

     

    GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

    Collection of TypeScript type challenges with online judge - GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

    github.com

     


    📘 목차 - Easy

    1. Pick
    2. Readonly
    3. Tuple to Object
    4. First of Array
    5. Length of Tuple
    6. Exclude
    7. Awaited

     

     

     

    📘 문제 및 풀이

     

    1. Pick

    Implement the built-in Pick<T, K> generic without using it.
    Constructs a type by picking the set of properties K from T

    For example:
    interface Todo {
      title: string
      description: string
      completed: boolean
    }
    
    type TodoPreview = MyPick<Todo, 'title' | 'completed'>
    
    const todo: TodoPreview = {
        title: 'Clean room',
        completed: false,
    }
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
      Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
      // @ts-expect-error
      MyPick<Todo, 'title' | 'completed' | 'invalid'>,
    ]
    
    interface Todo {
      title: string
      description: string
      completed: boolean
    }
    
    interface Expected1 {
      title: string
    }
    
    interface Expected2 {
      title: string
      completed: boolean
    }

     

     

    🖌 풀이

    type MyPick<T, K extends keyof T> = {
      [P in K]: T[P]
    }

    Pick 유틸리티 타입을 직접 작성하는 코드다. 제네릭의 인자를 각각 T(원본 타입), K(반환할 타입의 key들) 로 활용할 것이다.

     

    반환할 타입을 Object로 먼저 설정해준다. 이 타입의 key를 P(타입)로 명시하고, key는 K에서, value는 T[P]로 가져온다.

    마지막으로, K가 T의 키값임을 명시하기 위해 keyof T 타입을 extends로 "제약"시켜준다. (에러 케이스)

     

     

    * keyof

    Object 타입의 키값들을 유니온 타입으로 반환하는 문법

    interface Person {
      age: number;
      name: string;
    }
    
    type PersonKeys = keyof Person; // "age" | "name"

     

     

    * extends

    통상적으로 interface의 확장이나 상속을 위한 문법으로 알고 있었다.

    하지만, 제네릭 인자에 대해서는 타입을 제약하기 위한 문법으로도 사용된다. (참고링크)

    추가로, extends keyof는 일부 키값만 활용해도 에러가 발생하지 않으나, in keyof 는 모든 키를 사용하지 않으면 에러가 발생하는 차이가 있었다.

     

     

     

    2. Readonly

    Implement the built-in Readonly<T> generic without using it.
    Constructs a type with all properties of T set to readonly, meaning the properties of the constructed type cannot be reassigned.

    For example:
    interface Todo {
      title: string
      description: string
    }
    
    const todo: MyReadonly<Todo> = {
      title: "Hey",
      description: "foobar"
    }
    
    todo.title = "Hello" // Error: cannot reassign a readonly property
    todo.description = "barFoo" // Error: cannot reassign a readonly property
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
    ]
    
    interface Todo1 {
      title: string
      description: string
      completed: boolean
      meta: {
        author: string
      }
    }

     

     

    🖌 풀이

    type MyReadonly<T extends object> = {
      readonly [P in keyof T]: T[P]
    }

    Readonly 유틸리티 타입을 직접 작성하는 코드다.

    반환타입도 Object이며, key는 T의 in keyof로 가져오고 여기서 readonly 문법을 통해 읽기 전용 프로퍼티임을 명시해준다.

     

     

    * readonly

    읽기 전용 프로퍼티를 설정하기 위한 문법으로, 선언 후 변경이 불가능한 프로퍼티들을 설정하기 위해 사용된다.

    Class 초기화, interface 키 값 등에 적용할 수 있다. (참고링크)

     

     

     

    3. Tuple to Object

    Given an array, transform it into an object type and the key/value must be in the provided array.

    For example:
    const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
    
    type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
    const tupleNumber = [1, 2, 3, 4] as const
    const tupleMix = [1, '2', 3, '4'] as const
    
    type cases = [
      Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
      Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>,
      Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1; '2': '2'; 3: 3; '4': '4' }>>,
    ]
    
    // @ts-expect-error
    type error = TupleToObject<[[1, 2], {}]>

     

    🖌 풀이

    type TupleToObject<T extends readonly (string | number | symbol)[]> = {
      [P in T[number]]: P
    }

    튜플 타입을 받아서 각 요소를 key, value로 가지는 객체 타입을 반환해야한다.

     

    T[number] 로 튜플의 각 요소를 인덱싱해서 P라는 타입으로 지정한 뒤, 이를 key와 value로 넣어준다.

    제네릭에서 튜플타입을 받기 위해 readonly any[] 로 제약하지만, 에러 케이스처럼 튜플 요소들 외 타입을 제외하기 위해 any를 더 구체화한다. (string | number | symbol)[]

     

     

    * Tuple(튜플)

    튜플은 서로 다른 타입을 함께 가질 수 있는 배열이다. 튜플 타입을 선언할 때, 엘리먼트의 타입과 개수를 설정할 수 있다.

    let tuple: [string, number]
    
    tuple = ["hi", 200];		// ok!
    tuple = ["hi", "400"];		// error

     

     

     

    4. First of Array

    Implement a generic First<T> that takes an Array T and returns its first element's type.

    For example:
    type arr1 = ['a', 'b', 'c']
    type arr2 = [3, 2, 1]
    
    type head1 = First<arr1> // expected to be 'a'
    type head2 = First<arr2> // expected to be 3
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<First<[3, 2, 1]>, 3>>,
      Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
      Expect<Equal<First<[]>, never>>,
      Expect<Equal<First<[undefined]>, undefined>>,
    ]
    
    type errors = [
      // @ts-expect-error
      First<'notArray'>,
      // @ts-expect-error
      First<{ 0: 'arrayLike' }>,
    ]

     

    🖌 풀이

    type First<T extends any[]> = T extends [] ? never : T[0]

    제네릭으로 받은 배열(T)의 첫 번째 인자의 타입을 반환하면 된다.

    단, 3번째 케이스처럼 빈 배열인 경우엔 never를 반환해야 하므로, 조건부 타입(Conditional Types) 으로 지정해준다.

     

     

    * 조건부 타입(Conditional Types)

    입력된 제네릭 타입에 따라 타입을 결정하는 문법이다. 삼항연산자와 비슷한 문법이며, extends 키워드로 타입을 체크한다. (참고문서)

     

     

     

    5. Length of Tuple

    For given a tuple, you need create a generic Length, pick the length of the tuple

    For example:
    type tesla = ['tesla', 'model 3', 'model X', 'model Y']
    type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
    
    type teslaLength = Length<tesla>  // expected 4
    type spaceXLength = Length<spaceX> // expected 5
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
    const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const
    
    type cases = [
      Expect<Equal<Length<typeof tesla>, 4>>,
      Expect<Equal<Length<typeof spaceX>, 5>>,
      // @ts-expect-error
      Length<5>,
      // @ts-expect-error
      Length<'hello world'>,
    ]

     

    🖌 풀이

    type Length<T extends readonly any[]> = T['length']

    튜플은 배열처럼 보이지만 아래처럼 기술될 수 있다. 즉, 여기서 length 값이 튜플의 길이에 해당한다. (참고링크)

    {
      0: 'sun',
      1: 'mon',
      2: 'tue',
      3: 'wed',
      4: 'thu',
      5: 'fri',
      6: 'sat',
      readonly length: 7,
    }

     

     

     

    6. Exclude

    Implement the built-in Exclude<T, U>
    * Exclude from T those types that are assignable to U

    For example:
    type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
      Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>,
      Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>,
    ]

     

    🖌 풀이

    type MyExclude<T, U> = T extends U ? never : T

    조건부 타입의 특징인 분산 조건부 타입(Distributive Conditional Types) 를 활용한 풀이이다.

    유니온 타입(A | B | C)에 대해 조건부(T extends U ? X : Y)를 인스턴스화 하면 각 타입 인수에 대한 조건부로 걸린다. (참고링크)

    (A extends U ? X : Y | B extends U ? X : Y | C extends U ? X : Y)

     

     

     

    7. Awaited

    If we have a type which is wrapped type like Promise. How we can get a type which is inside the wrapped type?

    For example: if we have Promise<ExampleType> how to get ExampleType?
    type ExampleType = Promise<string>
    
    type Result = MyAwaited<ExampleType> // string
    
    
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type X = Promise<string>
    type Y = Promise<{ field: number }>
    type Z = Promise<Promise<string | number>>
    type Z1 = Promise<Promise<Promise<string | boolean>>>
    type T = { then: (onfulfilled: (arg: number) => any) => any }
    
    type cases = [
      Expect<Equal<MyAwaited<X>, string>>,
      Expect<Equal<MyAwaited<Y>, { field: number }>>,
      Expect<Equal<MyAwaited<Z>, string | number>>,
      Expect<Equal<MyAwaited<Z1>, string | boolean>>,
      Expect<Equal<MyAwaited<T>, number>>,
    ]
    
    // @ts-expect-error
    type error = MyAwaited<number>

     

    🖌 풀이

    type MyAwaited<T> = T extends PromiseLike<infer R>
      ? R extends PromiseLike<any>
        ? MyAwaited<R>
        : R
      : never

    우선, T의 Promise 여부를 확인해서 아닐 경우 never를 반환한다. Promise라면 infer 키워드를 통해 내부의 타입을 추론한다.

    type MyAwaited<T> = T extends Promise<infer InnerType> ? InnerType : never;

     

    위 타입도 케이스들은 해결이 가능하나, 내부 타입이 Promise인 경우를 위해 안쪽 조건부처럼 MyAwaited를 재귀적으로 실행해준다.

     

     

    * infer

    infer는 조건부 타입의 extends 절에서 특정 지점의 타입을 추론하기 위해 사용되는 키워드다. (어렵고.. 나도 이해하는데 꽤 걸렸다..)

     

    T extends infer U ? X : Y

    기본적인 문법은 위와 같고, 기존 조건부 타입과 큰 차이가 안느껴지지만 제네릭이나 함수 등의 예제로 가면 유용해진다.

     

    type Unpacked<T> =
        T extends (infer U)[] ? U :
        T extends (...args: any[]) => infer U ? U :
        T extends Promise<infer U> ? U :
        T;
    
    type T0 = Unpacked<string>;  // string
    type T1 = Unpacked<string[]>;  // string
    type T2 = Unpacked<() => string>;  // string
    type T3 = Unpacked<Promise<string>>;  // string
    type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
    type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

    Unpacked 유틸리티 타입은 T를 제네릭 인자로 받는다.

    이 T가 각각 배열인 경우엔 엘리먼트의, 함수인 경우 반환값의, 비동기(Promise)인 경우 반환값을 추론하는 타입이다.

     

    타입을 직접 명시하는 게 아닌, 런타임 과정에서 타입을 추론하기 위해 이 키워드가 사용된다. (참고링크)

     

     

    * Promise vs PromiseLike

    Promise 타입을 다루기 위해 Promise 제네릭도 있지만, PromiseLike도 있다.

    이 두 타입은 Promise<T> 는 finally만, PromiseLike<T> 는 then만 있다는 차이가 있다.

    // lib.2018.promise.d.ts
    interface Promise<T> {
      /**
       * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
       * resolved value cannot be modified from the callback.
       * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
       * @returns A Promise for the completion of the callback.
       */
      finally(onfinally?: (() => void) | undefined | null): Promise<T>
    }
    
    // lib.es5.d.ts
    interface Promise<T> {
      /**
       * Attaches callbacks for the resolution and/or rejection of the Promise.
       * @param onfulfilled The callback to execute when the Promise is resolved.
       * @param onrejected The callback to execute when the Promise is rejected.
       * @returns A Promise for the completion of which ever callback is executed.
       */
      then<TResult1 = T, TResult2 = never>(
        onfulfilled?:
          | ((value: T) => TResult1 | PromiseLike<TResult1>)
          | undefined
          | null,
        onrejected?:
          | ((reason: any) => TResult2 | PromiseLike<TResult2>)
          | undefined
          | null,
      ): Promise<TResult1 | TResult2>
    
      /**
       * Attaches a callback for only the rejection of the Promise.
       * @param onrejected The callback to execute when the Promise is rejected.
       * @returns A Promise for the completion of the callback.
       */
      catch<TResult = never>(
        onrejected?:
          | ((reason: any) => TResult | PromiseLike<TResult>)
          | undefined
          | null,
      ): Promise<T | TResult>
    }
    interface PromiseLike<T> {
      /**
       * Attaches callbacks for the resolution and/or rejection of the Promise.
       * @param onfulfilled The callback to execute when the Promise is resolved.
       * @param onrejected The callback to execute when the Promise is rejected.
       * @returns A Promise for the completion of which ever callback is executed.
       */
      then<TResult1 = T, TResult2 = never>(
        onfulfilled?:
          | ((value: T) => TResult1 | PromiseLike<TResult1>)
          | undefined
          | null,
        onrejected?:
          | ((reason: any) => TResult2 | PromiseLike<TResult2>)
          | undefined
          | null,
      ): PromiseLike<TResult1 | TResult2>
    }

    그렇기에 이 둘의 가장 큰 차이는, 1) PromiseLike는 catch를 관여할 수 없다, 2) Promise는 비동기 반환 결과를 받을 수 없다 이다.

     

    Promise가 정식 스펙이 되기 전 이를 구현한 라이브러리들이 있었고,

    이들 중에 catch를 다루지 않는 타입을 지원하기 위해 PromiseLike가 별도로 출시된 것이다.

    반응형
Designed by Tistory.