ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Type-challenges] 난이도 Medium - (4)
    Front-End(Web)/Typescript 2023. 2. 16. 18:50
    반응형

    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

     


    📘 목차 - Medium

    1. KebabCase
    2. Diff
    3. AnyOf
    4. IsNever
    5. IsUnion
    6. ReplaceKeys
    7. Remove Index Signature

     

     

    📘 문제 및 풀이

     

    1. KebabCase

    Replace the camelCase or PascalCase string with kebab-case.
    FooBarBaz -> foo-bar-baz

    For example
    type FooBarBaz = KebabCase<"FooBarBaz">;
    const foobarbaz: FooBarBaz = "foo-bar-baz";
    
    type DoNothing = KebabCase<"do-nothing">;
    const doNothing: DoNothing = "do-nothing";
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<KebabCase<'FooBarBaz'>, 'foo-bar-baz'>>,
      Expect<Equal<KebabCase<'fooBarBaz'>, 'foo-bar-baz'>>,
      Expect<Equal<KebabCase<'foo-bar'>, 'foo-bar'>>,
      Expect<Equal<KebabCase<'foo_bar'>, 'foo_bar'>>,
      Expect<Equal<KebabCase<'Foo-Bar'>, 'foo--bar'>>,
      Expect<Equal<KebabCase<'ABC'>, 'a-b-c'>>,
      Expect<Equal<KebabCase<'-'>, '-'>>,
      Expect<Equal<KebabCase<''>, ''>>,
      Expect<Equal<KebabCase<'😎'>, '😎'>>,
    ]

     

    🖌 풀이

    type KebabCase<S, O = S> = S extends `${infer F}${infer R}` ?
      F extends Uppercase<F> ?
        F extends '-' | '_' | Uncapitalize<F> ? `${F}${KebabCase<R, O>}` :
          S extends O ? `${Lowercase<F>}${KebabCase<R, O>}` : `-${Lowercase<F>}${KebabCase<R, O>}`
          : `${F}${KebabCase<R, O>}`
      : ''

    1) F(첫글자)의 대소문자 여부, 특수문자(-, _, Uncapitalize) 여부, 2) S(현재글자) 가 원본(O)과 같은지 여부(처음 실행인지) 등

    계속 해서 분기하는 문제다. 아래의 간결한 모범답안을 같이 참고하는게 좋다.

    type KebabCase<S extends string> = S extends `${infer L}${infer R}`
        ? R extends Uncapitalize<R>
            ? `${Uncapitalize<L>}${KebabCase<R>}`
            : `${Uncapitalize<L>}-${KebabCase<Uncapitalize<R>>}`
        : S

    Uncapitalize 유틸리티로 첫 글자만 소문자로 바꿀 수 있어 쉽게 풀 수 있었다.

     

     

    * Uppercase, Lowercase + Capitalize, Uncapitalize

     

    Uppercase, Lowercase는 제네릭 인자의 모든 글자를 대/소문자로 바꾸는 것이다.

    반면, Capitalize, Uncapitalize는 제네릭 인자의 첫 글자만 대/소문자로 바꾼다는 차이가 있다. (참고링크)

     


    2. Diff

    Get an Object that is the difference between O & O1
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type Foo = {
      name: string
      age: string
    }
    type Bar = {
      name: string
      age: string
      gender: number
    }
    type Coo = {
      name: string
      gender: number
    }
    
    type cases = [
      Expect<Equal<Diff<Foo, Bar>, { gender: number }>>,
      Expect<Equal<Diff<Bar, Foo>, { gender: number }>>,
      Expect<Equal<Diff<Foo, Coo>, { age: string; gender: number }>>,
      Expect<Equal<Diff<Coo, Foo>, { age: string; gender: number }>>,
    ]

     

    🖌 풀이

    type Diff<O, O1> = {
      [P in (Exclude<keyof O, keyof O1> | Exclude<keyof O1, keyof O>)]: P extends keyof O1 ? O1[P] : P extends keyof O ? O[P] : never;
    }

    키타입(P)을 설정할 때 O, O1에서 각각 해당되지 않는 값들을 Exclude로 넣어준다.

    또한, O, O1에 각각 해당하는 값타입(O[P], O1[P]) 를 알맞게 할당해주면 된다.

     


    3. AnyOf

    Implement Python liked any function in the type system. A type takes the Array and returns true if any element of the Array is true. If the Array is empty, return false.

    For example:
    type Sample1 = AnyOf<[1, '', false, [], {}]> // expected to be true.
    type Sample2 = AnyOf<[0, '', false, [], {}]> // expected to be false.
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<AnyOf<[1, 'test', true, [1], { name: 'test' }, { 1: 'test' }]>, true>>,
      Expect<Equal<AnyOf<[1, '', false, [], {}]>, true>>,
      Expect<Equal<AnyOf<[0, 'test', false, [], {}]>, true>>,
      Expect<Equal<AnyOf<[0, '', true, [], {}]>, true>>,
      Expect<Equal<AnyOf<[0, '', false, [1], {}]>, true>>,
      Expect<Equal<AnyOf<[0, '', false, [], { name: 'test' }]>, true>>,
      Expect<Equal<AnyOf<[0, '', false, [], { 1: 'test' }]>, true>>,
      Expect<Equal<AnyOf<[0, '', false, [], { name: 'test' }, { 1: 'test' }]>, true>>,
      Expect<Equal<AnyOf<[0, '', false, [], {}, undefined, null]>, false>>,
      Expect<Equal<AnyOf<[]>, false>>,
    ]

     

    🖌 풀이

    type Falsy = 0 | '' | false | [] | undefined | null | {[P in any]: never}
    
    type AnyOf<T extends readonly any[]> =
      T extends [infer F, ...infer R]
      ? F extends Falsy
        ? AnyOf<R>
        : true
      : false;

    falsy한 값들을 우선 Falsy라는 유니온 타입으로 정의한다.

    첫 엘리먼트(F)가 Falsy에 해당하는지 여부에 따라 재귀적으로 실행해주면 된다.

    단, 빈 객체를 설정할 땐 {} 가 아니라 { [P in any]: never } 로 설정해줘야 객체 케이스들을 통과할 수 있다.

     


    4. IsNever

    Implement a type IsNever, which takes input type T. If the type of resolves to never, return true, otherwise false.

    For example:
    type A = IsNever<never>  // expected to be true
    type B = IsNever<undefined> // expected to be false
    type C = IsNever<null> // expected to be false
    type D = IsNever<[]> // expected to be false
    type E = IsNever<number> // expected to be false
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<IsNever<never>, true>>,
      Expect<Equal<IsNever<never | string>, false>>,
      Expect<Equal<IsNever<''>, false>>,
      Expect<Equal<IsNever<undefined>, false>>,
      Expect<Equal<IsNever<null>, false>>,
      Expect<Equal<IsNever<[]>, false>>,
      Expect<Equal<IsNever<{}>, false>>,
    ]

     

    🖌 풀이

    type IsNever<T> = [T] extends [never] ? true : false

    쉽게 풀 수 있는 문제다. T를 바로 비교하면 never는 기본적으로 서브타입이므로 never 자체를 제외하곤 어떤 값도 할당할 수 없다.

    이를 튜플로 감싸줘야 T와 never 자체가 비교되어 첫 케이스가 통과한다. (참고링크)

     


    5. IsUnion

    Implement a type IsUnion, which takes an input type T and returns whether T resolves to a union type.

    For example:
    type case1 = IsUnion<string>  // false
    type case2 = IsUnion<string|number>  // true
    type case3 = IsUnion<[string|number]>  // false
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type cases = [
      Expect<Equal<IsUnion<string>, false>>,
      Expect<Equal<IsUnion<string | number>, true>>,
      Expect<Equal<IsUnion<'a' | 'b' | 'c' | 'd'>, true>>,
      Expect<Equal<IsUnion<undefined | null | void | ''>, true>>,
      Expect<Equal<IsUnion<{ a: string } | { a: number }>, true>>,
      Expect<Equal<IsUnion<{ a: string | number }>, false>>,
      Expect<Equal<IsUnion<[string | number]>, false>>,
      // Cases where T resolves to a non-union type.
      Expect<Equal<IsUnion<string | never>, false>>,
      Expect<Equal<IsUnion<string | unknown>, false>>,
      Expect<Equal<IsUnion<string | any>, false>>,
      Expect<Equal<IsUnion<string | 'a'>, false>>,
      Expect<Equal<IsUnion<never>, false>>,
    ]

     

    🖌 풀이

    type IsUnion<T, U = T> = [T] extends [never] ? false : T extends U ? ([Exclude<U, T>] extends [never] ? false : true) : false;

    제네릭 인자에 유니온이 들어오면, T는 각 타입이 되는 특성을 이용해 푸는 문제다.

     

    T에 never가 있다면 false를, T가 U에 해당한다면 이를 제외(Exclude)한 타입에 never가 포함됬는지 여부를 반환한다.

     


    6. ReplaceKeys

    Implement a type ReplaceKeys, that replace keys in union types, if some type has not this key, just skip replacing, A type takes three arguments.

    For example:
    type NodeA = {
      type: 'A'
      name: string
      flag: number
    }
    
    type NodeB = {
      type: 'B'
      id: number
      flag: number
    }
    
    type NodeC = {
      type: 'C'
      name: string
      flag: number
    }
    
    
    type Nodes = NodeA | NodeB | NodeC
    
    type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.
    
    type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type NodeA = {
      type: 'A'
      name: string
      flag: number
    }
    
    type NodeB = {
      type: 'B'
      id: number
      flag: number
    }
    
    type NodeC = {
      type: 'C'
      name: string
      flag: number
    }
    
    type ReplacedNodeA = {
      type: 'A'
      name: number
      flag: string
    }
    
    type ReplacedNodeB = {
      type: 'B'
      id: number
      flag: string
    }
    
    type ReplacedNodeC = {
      type: 'C'
      name: number
      flag: string
    }
    
    type NoNameNodeA = {
      type: 'A'
      flag: number
      name: never
    }
    
    type NoNameNodeC = {
      type: 'C'
      flag: number
      name: never
    }
    
    type Nodes = NodeA | NodeB | NodeC
    type ReplacedNodes = ReplacedNodeA | ReplacedNodeB | ReplacedNodeC
    type NodesNoName = NoNameNodeA | NoNameNodeC | NodeB
    
    type cases = [
      Expect<Equal<ReplaceKeys<Nodes, 'name' | 'flag', { name: number; flag: string }>, ReplacedNodes>>,
      Expect<Equal<ReplaceKeys<Nodes, 'name', { aa: number }>, NodesNoName>>,
    ]

     

    🖌 풀이

    type ReplaceKeys<U, T, Y> = {
      [P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P];
    };

    문제를 먼저 이해해야했다. 유니온 객체(U) 키들이 조건(T)에 해당하는 경우 대체객체(Y)에 해당 키가 있는 경우 그 타입으로 or 아니면 never로 변환된 유니온 객체를 반환하는 것이다.

     

    먼저, 키(P)가 T에 포함되는지를 확인한다. 이에 해당된다면, P가 Y의 키인 경우 그 값을 아니면 never를 대체해준다. 

    T에 포함되지 않는 키라면 기존의 값을 그대로 넣어준다.

     


    7. Remove Index Signature

    Implement RemoveIndexSignature<T> , exclude the index signature from object types.

    For example:
    type Foo = {
      [key: string]: any;
      foo(): void;
    }
    
    type A = RemoveIndexSignature<Foo>  // expected { foo(): void }
    /* _____________ Test Cases _____________ */
    import type { Equal, Expect } from '@type-challenges/utils'
    
    type Foo = {
      [key: string]: any
      foo(): void
    }
    
    type Bar = {
      [key: number]: any
      bar(): void
      0: string
    }
    
    const foobar = Symbol('foobar')
    type FooBar = {
      [key: symbol]: any
      [foobar](): void
    }
    
    type Baz = {
      bar(): void
      baz: string
    }
    
    type cases = [
      Expect<Equal<RemoveIndexSignature<Foo>, { foo(): void }>>,
      Expect<Equal<RemoveIndexSignature<Bar>, { bar(): void; 0: string }>>,
      Expect<Equal<RemoveIndexSignature<FooBar>, { [foobar](): void }>>,
      Expect<Equal<RemoveIndexSignature<Baz>, { bar(): void; baz: string }>>,
    ]

     

    🖌 풀이

    type RemoveIndexSignature<T> = {
      [k in keyof T as string extends k
        ? never
        : number extends k
        ? never
        : symbol extends k
        ? never
        : k]: T[k];
    };

    제네릭 인자에서 Index Signature 부분만 제거하는 유틸리티다.

    키(k)가 string, number, symbol 등 Index Signature를 포함하면 never를 아니면 키 자체를 키값으로 사용한다.

    값은 T[k] 그대로 사용하면 된다.

     

     

    * Index Signature

     

    객체의 프로퍼티를 명확히 선언할 수 없을 때, string, number 등 공통타입 혹은 유니온으로 선언하기 위해 사용하는 문법 (링크)

    => 문제점 : 빈 객체 에러 미발생, key 각각에 대한 타입명시 불가, 공통타입을 사용할 경우 키 값 오류 발생가능

    반응형
Designed by Tistory.