-
[Type-challenges] 난이도 Medium - (4)Front-End(Web)/Typescript 2023. 2. 16. 18:50반응형
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- KebabCase
- Diff
- AnyOf
- IsNever
- IsUnion
- ReplaceKeys
- Remove Index Signature
📘 문제 및 풀이
1. KebabCase
Replace the camelCase or PascalCase string with kebab-case.
FooBarBaz -> foo-bar-baz
For exampletype 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 각각에 대한 타입명시 불가, 공통타입을 사용할 경우 키 값 오류 발생가능
반응형'Front-End(Web) > Typescript' 카테고리의 다른 글
[Type-challenges] 난이도 Medium - (6) (0) 2023.02.27 [Type-challenges] 난이도 Medium - (5) (0) 2023.02.23 [Type-challenges] 난이도 Medium - (3) (0) 2023.02.14 [Type-challenges] 난이도 Medium - (2) (0) 2023.02.14 [Type-challenges] 난이도 Medium - (1) (0) 2023.02.06