[Type-challenges] 난이도 Medium - (3)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- Permutation
- Length of String
- Flatten
- Append to Object
- Absolute
- String To Union
- Merge
📘 문제 및 풀이
1. Permutation
Implement permutation type that transforms union types into the array that includes permutations of unions.
type perm = Permutation<'A' | 'B' | 'C'>;
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Permutation<'A'>, ['A']>>,
Expect<Equal<Permutation<'A' | 'B' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
Expect<Equal<Permutation<'B' | 'A' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
Expect<Equal<Permutation<boolean>, [false, true] | [true, false]>>,
Expect<Equal<Permutation<never>, []>>,
]
🖌 풀이
type Permutation<T, U = T> = [T] extends [never] ? [] : T extends U ? [T, ...Permutation<Exclude<U, T>>] : never
먼저, Permutation은 제네릭 인자를 T로 받되, 이를 분기할 때 참조하기 위해 두 번째 인자(U)에 모든 T를 복사해둔다.
마지막 케이스를 대응하기 위해 T에 never가 포함되는 경우 [] 을 반환한다. 단, 이를 판단하기 위해 T, never를 배열로 감싸줘야한다.
또한, T가 U에 해당하면 재귀적으로 실행해주고 이 때는 T와 현재 T를 제외한 Permutation 배열 타입으로 설정한다.
끝으로, 실행할 필요가 없으면 never로 제외시켜주면 된다. (풀이 참고링크)
2. Length of String
Compute the length of a string literal, which behaves like String#length.
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<LengthOfString<''>, 0>>,
Expect<Equal<LengthOfString<'kumiko'>, 6>>,
Expect<Equal<LengthOfString<'reina'>, 5>>,
Expect<Equal<LengthOfString<'Sound! Euphonium'>, 16>>,
]
🖌 풀이
type LengthOfString<
S extends string,
T extends string[] = []
> = S extends `${infer F}${infer R}`
? LengthOfString<R, [F, ...T]>
: T["length"];
LengthOfString은 제네릭 인자 2가지를 받는다. S는 문자열을, T는 문자열을 하나씩 자른 배열(split) 이라고 생각하면 된다.
S를 먼저 F와 R로 나누고, 남은 R에 대해서 재귀적으로 LengthOfString을 실행한다. 이 때, T 배열에는 F를 추가해준다.
마지막으로 끝나서 빈 문자열이 되면 T의 length를 반환하면 된다.
3. Flatten
In this challenge, you would need to write a type that takes an array and emitted the flatten array type.
For example:
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Flatten<[]>, []>>,
Expect<Equal<Flatten<[1, 2, 3, 4]>, [1, 2, 3, 4]>>,
Expect<Equal<Flatten<[1, [2]]>, [1, 2]>>,
Expect<Equal<Flatten<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, 5]>>,
Expect<Equal<Flatten<[{ foo: 'bar'; 2: 10 }, 'foobar']>, [{ foo: 'bar'; 2: 10 }, 'foobar']>>,
]
🖌 풀이
type Flatten<T extends any[], A extends any[] = []> =
T extends [infer F, ...infer R]
? F extends any[]
? Flatten<[...F, ...R], A>
: Flatten<R, [...A, F]>
: A
Flatten은 제네릭 인자로 T(원본배열), A(flat하는 배열) 2가지를 받는다.
T를 F(첫 번째 인자)와 R(나머지 인자들) 로 나눈 뒤, T에 배열이 남아있으면 Flatten을 재귀적으로, 아니면 A를 반환한다.
단, Flatten을 재귀할 때, 케이스 3, 4처럼 다중 배열인 경우가 있다.
F가 배열이면 원본에서 첫 번째 인자인 F를 한 번 flat해서 현재 F에 대해 다시 실행시키고,
F가 배열이 아닌 요소면 첫 번째 인자는 나머지 R들을, 두 번째 인자인 A에 F를 추가해준다.
4. Append to Object
Implement a type that adds a new field to the interface. The type takes the three arguments. The output should be an object with the new field.
For example
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type test1 = {
key: 'cat'
value: 'green'
}
type testExpect1 = {
key: 'cat'
value: 'green'
home: boolean
}
type test2 = {
key: 'dog' | undefined
value: 'white'
sun: true
}
type testExpect2 = {
key: 'dog' | undefined
value: 'white'
sun: true
home: 1
}
type test3 = {
key: 'cow'
value: 'yellow'
sun: false
}
type testExpect3 = {
key: 'cow'
value: 'yellow'
sun: false
isMotherRussia: false | undefined
}
type cases = [
Expect<Equal<AppendToObject<test1, 'home', boolean>, testExpect1>>,
Expect<Equal<AppendToObject<test2, 'home', 1>, testExpect2>>,
Expect<Equal<AppendToObject<test3, 'isMotherRussia', false | undefined>, testExpect3>>,
]
🖌 풀이
type AppendToObject<T, U extends string, V> = {
[P in keyof T | U]: P extends keyof T ? T[P] : V
}
AppendToObject 의 제네릭 인자는 3개로, T(추가 전 객체), U(추가할 key, string으로 특정), V(추가할 value) 이다.
새로운 객체의 키인 P는 keyof T와 U 중 하나에 해당될 것이고, 그 값은 P가 T의 key인 경우 T[P]를 아니면 새로운 V를 넣는다.
5. Absolute
Implement the Absolute type. A type that take string, number or bigint. The output should be a positive number string
For example
type Test = -100;
type Result = Absolute<Test>; // expected to be "100"
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Absolute<0>, '0'>>,
Expect<Equal<Absolute<-0>, '0'>>,
Expect<Equal<Absolute<10>, '10'>>,
Expect<Equal<Absolute<-5>, '5'>>,
Expect<Equal<Absolute<'0'>, '0'>>,
Expect<Equal<Absolute<'-0'>, '0'>>,
Expect<Equal<Absolute<'10'>, '10'>>,
Expect<Equal<Absolute<'-5'>, '5'>>,
Expect<Equal<Absolute<-1_000_000n>, '1000000'>>,
Expect<Equal<Absolute<9_999n>, '9999'>>,
]
🖌 풀이
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer N}` ? N : `${T}`
abs() 와 같은 유틸리티가 있을 줄 알았는데 생각보다 심플한 문제였다.
T를 문자열화 한 뒤, 맨 앞에 '-' 마이너스가 있으면 이를 뺀 부분을, 아니면 문자열한 T를 반환한다.
6. String to Union
Implement the String to Union type. Type take string argument. The output should be a union of input letters
For example
type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<StringToUnion<''>, never>>,
Expect<Equal<StringToUnion<'t'>, 't'>>,
Expect<Equal<StringToUnion<'hello'>, 'h' | 'e' | 'l' | 'l' | 'o'>>,
Expect<Equal<StringToUnion<'coronavirus'>, 'c' | 'o' | 'r' | 'o' | 'n' | 'a' | 'v' | 'i' | 'r' | 'u' | 's'>>,
]
🖌 풀이
type StringToUnion<T extends string> = T extends `${infer F}${infer R}` ? F | StringToUnion<R> : never;
T의 모든 문자를 split() 한 유니온 타입을 만드는 문제다.
문자열이 있다면 앞의 F 그리고 뒤의 R은 재귀적으로 StringToUnion을 적용한 유니온을 반환한다.
7. Merge
Merge two types into a new type. Keys of the second type overrides keys of the first type.
For example
type foo = {
name: string;
age: string;
}
type coo = {
age: number;
sex: string
}
type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string}
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type Foo = {
a: number
b: string
}
type Bar = {
b: number
c: boolean
}
type cases = [
Expect<Equal<Merge<Foo, Bar>, {
a: number
b: number
c: boolean
}>>,
]
🖌 풀이
type Merge<F extends object, S extends object> = {
[P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
};
마찬가지로 어려울 듯 했으나, 삼항연산을 두 번 활용하면 쉽게 풀 수 있는 문제였다.