[Type-challenges] 난이도 Hard - (3)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📕 목차 - Hard
- Tuple Filter
- Tuple to Enum Object
- printf
- Deep Object to Unique
- Length of String 2
- Union to Tuple
📕 문제 및 풀이
1. Tuple Filter
Implement a type FilterOut<T, F> that filters out items of the given type F from the tuple T.
For example,
type Filtered = FilterOut<[1, 2, null, 3], null> // [1, 2, 3]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<FilterOut<[], never>, []>>,
Expect<Equal<FilterOut<[never], never>, []>>,
Expect<Equal<FilterOut<['a', never], never>, ['a']>>,
Expect<Equal<FilterOut<[1, never, 'a'], never>, [1, 'a']>>,
Expect<Equal<FilterOut<[never, 1, 'a', undefined, false, null], never | null | undefined>, [1, 'a', false]>>,
Expect<Equal<FilterOut<[number | null | undefined, never], never | null | undefined>, [number | null | undefined]>>,
]
🖌 풀이
type FilterOut<T extends any[], F> = T extends [infer L, ...infer R]
? [L] extends [F] ? FilterOut<R, F> : [L, ...FilterOut<R, F>]
: []
T의 각 인자를 순회하면서 F와 비교하면 된다. 단, never의 케이스가 있으므로 이들을 배열([])로 감싼 뒤 비교해준다.
2. Tuple to Enum Object
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
const OperatingSystem = ['macOS', 'Windows', 'Linux'] as const
const Command = ['echo', 'grep', 'sed', 'awk', 'cut', 'uniq', 'head', 'tail', 'xargs', 'shift'] as const
type cases = [
Expect<Equal<Enum<[]>, {}>>,
Expect<Equal<
Enum<typeof OperatingSystem>,
{
readonly MacOS: 'macOS'
readonly Windows: 'Windows'
readonly Linux: 'Linux'
}
>>,
Expect<Equal<
Enum<typeof OperatingSystem, true>,
{
readonly MacOS: 0
readonly Windows: 1
readonly Linux: 2
}
>>,
Expect<Equal<
Enum<typeof Command>,
{
readonly Echo: 'echo'
readonly Grep: 'grep'
readonly Sed: 'sed'
readonly Awk: 'awk'
readonly Cut: 'cut'
readonly Uniq: 'uniq'
readonly Head: 'head'
readonly Tail: 'tail'
readonly Xargs: 'xargs'
readonly Shift: 'shift'
}
>>,
Expect<Equal<
Enum<typeof Command, true>,
{
readonly Echo: 0
readonly Grep: 1
readonly Sed: 2
readonly Awk: 3
readonly Cut: 4
readonly Uniq: 5
readonly Head: 6
readonly Tail: 7
readonly Xargs: 8
readonly Shift: 9
}
>>,
]
🖌 풀이
type Enum<T extends readonly string[], N extends boolean = false> = {
readonly [K in keyof T as K extends `${number}` ? Capitalize<T[K]> : never]:
N extends false ? T[K] : K extends `${infer I extends number}` ? I : never
}
좋은 풀이가 있어 소개해보고자 한다!
먼저 키는, K가 인덱스(숫자) 이므로, T[K]를 Capitalize 한 값을 키로 사용한다.
그리고 값은, N이 true일 땐 인덱스, false일 땐 T[K] 값을 사용하면 된다. 단, K가 인덱스 유니온이므로 I(한정타입)으로 지정하는 절차가 필요하다.
3. printf
Implement Format<T extends string> generic.
For example,
type FormatCase1 = Format<"%sabc"> // FormatCase1 : string => string
type FormatCase2 = Format<"%s%dabc"> // FormatCase2 : string => number => string
type FormatCase3 = Format<"sdabc"> // FormatCase3 : string
type FormatCase4 = Format<"sd%abc"> // FormatCase4 : string
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Format<'abc'>, string>>,
Expect<Equal<Format<'a%sbc'>, (s1: string) => string>>,
Expect<Equal<Format<'a%dbc'>, (d1: number) => string>>,
Expect<Equal<Format<'a%%dbc'>, string>>,
Expect<Equal<Format<'a%%%dbc'>, (d1: number) => string>>,
Expect<Equal<Format<'a%dbc%s'>, (d1: number) => (s1: string) => string>>,
]
🖌 풀이
type Format<T extends string> = T extends `${any}%${infer F}${infer R}` ?
(F extends `${`d` | `s`}` ?
(fill: F extends `d` ? number : string) => Format<R> :
Format<R>) :
string;
이전의, Cprintf-Parser 와 유사한 풀이이다. (참고링크)
% 다음 문자인 F에 대해, F가 d | s 에 해당하면 재귀한 Format<R>을 반환하는 함수를, 아니면 Format<R> 자체 재귀만 진행한다.
함수의 매개변수 타입은 d면 number, s면 string을 적용한다.
문자열이 아닌 경우, string을 반환해줘야 예외처리 혹은 최종 반환값이 string이 된다.
4. Deep Object To Unique
/* _____________ Test Cases _____________ */
import type { Equal, IsFalse, IsTrue } from '@type-challenges/utils'
type Quz = { quz: 4 }
type Foo = { foo: 2; baz: Quz; bar: Quz }
type Bar = { foo: 2; baz: Quz; bar: Quz & { quzz?: 0 } }
type UniqQuz = DeepObjectToUniq<Quz>
type UniqFoo = DeepObjectToUniq<Foo>
type UniqBar = DeepObjectToUniq<Bar>
declare let foo: Foo
declare let uniqFoo: UniqFoo
uniqFoo = foo
foo = uniqFoo
type cases = [
IsFalse<Equal<UniqQuz, Quz>>,
IsFalse<Equal<UniqFoo, Foo>>,
IsTrue<Equal<UniqFoo['foo'], Foo['foo']>>,
IsTrue<Equal<UniqFoo['bar']['quz'], Foo['bar']['quz']>>,
IsFalse<Equal<UniqQuz, UniqFoo['baz']>>,
IsFalse<Equal<UniqFoo['bar'], UniqFoo['baz']>>,
IsFalse<Equal<UniqBar['baz'], UniqFoo['baz']>>,
IsTrue<Equal<keyof UniqBar['baz'], keyof UniqFoo['baz']>>,
IsTrue<Equal<keyof Foo, keyof UniqFoo & string>>,
]
🖌 풀이
declare const KEY: unique symbol;
type DeepObjectToUniq<O extends object> = {
[K in keyof O]:
O[K] extends object
? DeepObjectToUniq<O[K]> & {readonly [KEY]?: [O, K]}
: O[K]
} & {readonly [KEY]?: [O]}
Typescript는 다른 타입언어와 다르게 Structual Type System을 가지고 있다. (구조적으로 호환되면 같은 타입으로 적용가능)
이를 Nominal Type System을 적용해주는 함수를 만드는 문제이다.
(Bar를 위 유틸리티로 깊은 복사한 UniqueBar는 구조가 같아도 다른 타입으로 취급되어야함)
객체(O)의 각 키를 순회하며 재귀적으로 복사하는 건 유사하나, 객체의 끝에 { readonly [KEY]?: [O, (고유값(K))] } 를 교차타입으로 넣어주는 작업이 추가적으로 필요하다.
5. Length of String 2
Implement a type LengthOfString<S> that calculates the length of the template string (as in 298 - Length of String):
type T0 = LengthOfString<"foo"> // 3
The type must support strings several hundred characters long (the usual recursive calculation of the string length is limited by the depth of recursive function calls in TS, that is, it supports strings up to about 45 characters long).
/* _____________ Test Cases _____________ */
import type { Equal, IsTrue } from '@type-challenges/utils'
type cases = [
IsTrue<Equal<LengthOfString<''>, 0>>,
IsTrue<Equal<LengthOfString<'1'>, 1>>,
IsTrue<Equal<LengthOfString<'12'>, 2>>,
IsTrue<Equal<LengthOfString<'123'>, 3>>,
IsTrue<Equal<LengthOfString<'1234'>, 4>>,
IsTrue<Equal<LengthOfString<'12345'>, 5>>,
IsTrue<Equal<LengthOfString<'123456'>, 6>>,
IsTrue<Equal<LengthOfString<'1234567'>, 7>>,
IsTrue<Equal<LengthOfString<'12345678'>, 8>>,
IsTrue<Equal<LengthOfString<'123456789'>, 9>>,
IsTrue<Equal<LengthOfString<'1234567890'>, 10>>,
IsTrue<Equal<LengthOfString<'12345678901'>, 11>>,
IsTrue<Equal<LengthOfString<'123456789012'>, 12>>,
IsTrue<Equal<LengthOfString<'1234567890123'>, 13>>,
IsTrue<Equal<LengthOfString<'12345678901234'>, 14>>,
IsTrue<Equal<LengthOfString<'123456789012345'>, 15>>,
IsTrue<Equal<LengthOfString<'1234567890123456'>, 16>>,
IsTrue<Equal<LengthOfString<'12345678901234567'>, 17>>,
IsTrue<Equal<LengthOfString<'123456789012345678'>, 18>>,
IsTrue<Equal<LengthOfString<'1234567890123456789'>, 19>>,
IsTrue<Equal<LengthOfString<'12345678901234567890'>, 20>>,
IsTrue<Equal<LengthOfString<'123456789012345678901'>, 21>>,
IsTrue<Equal<LengthOfString<'1234567890123456789012'>, 22>>,
IsTrue<Equal<LengthOfString<'12345678901234567890123'>, 23>>,
IsTrue<Equal<LengthOfString<'aaaaaaaaaaaaggggggggggggggggggggkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'>, 272>>,
IsTrue<Equal<LengthOfString<'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'>, 999>>,
]
🖌 풀이
type LengthOfString<S extends string, L extends string[] = []> = S extends `${infer F}${infer R}` ? LengthOfString<R, [...L, F]> : L['length']
L은 S의 각 문자열을 요소로 누적하는 배열이다. 마지막에 이 길이(length)만 반환해주면 된다.
6. Union to Tuple
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type ExtractValuesOfTuple<T extends any[]> = T[keyof T & number]
type cases = [
Expect<Equal<UnionToTuple<'a' | 'b'>['length'], 2>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<'a' | 'b'>>, 'a' | 'b'>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<'a'>>, 'a'>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any>>, any>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<undefined | void | 1>>, void | 1>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any | 1>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<'d' | 'f' | 1 | never>>, 'f' | 'd' | 1>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<[{ a: 1 }] | 1>>, [{ a: 1 }] | 1>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<never>>, never>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<'a' | 'b' | 'c' | 1 | 2 | 'd' | 'e' | 'f' | 'g'>>, 'f' | 'e' | 1 | 2 | 'g' | 'c' | 'd' | 'a' | 'b'>>,
]
🖌 풀이
type Get<T, K extends string> = K extends keyof T
? T[K]
: K extends `${infer KEY}.${infer R}`
? KEY extends keyof T
? Get<T[KEY], R>
: never
: never;
유틸리티는 T(대상 객체), K(키네임을 저장하는 문자열) 2가지를 제너릭 인자로 받는다.
저장된 K가 T의 키값인 경우 T[P]를, 아니라면 never를 반환한다.(마지막 케이스)
다음으로, K문자열의 점('.') 을 찾아 앞부분을 KEY 문자열로 식별하고, 이 값이 T의 키값인 경우 이 T[KEY] 와 나머지 문자열(R) 에 대해 재귀적으로 유틸리티를 적용한다.