Front-End(Web)/Typescript

[Type-challenges] 난이도 Medium - (4)

ttaeng_99 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 각각에 대한 타입명시 불가, 공통타입을 사용할 경우 키 값 오류 발생가능

반응형