[Type-challenges] 난이도 Medium - (4)
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 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 각각에 대한 타입명시 불가, 공통타입을 사용할 경우 키 값 오류 발생가능