타입 챌린지 Medium 풀이
Medium 난이도부터는 여러 타입 도구를 조합해야 합니다. 재귀 타입, 분산 조건부 타입, 템플릿 리터럴 타입 등 고급 패턴이 본격적으로 등장합니다.
1. Get Return Type
섹션 제목: “1. Get Return Type”함수의 반환 타입을 추출합니다. never와 unknown을 any 대신 사용하는 것이 권장됩니다.
const fn = (v: boolean) => { if (v) return 1; else return 2;};
type MyReturnType<T extends (...args: never[]) => unknown> = T extends ( ...args: any[]) => infer R ? R : never;
type a = MyReturnType<typeof fn>; // 1 | 22. Omit
섹션 제목: “2. Omit”지정한 키를 제외한 나머지 프로퍼티를 가진 타입을 만듭니다. as 절을 이용한 키 리매핑이 핵심입니다.
interface Todo { title: string; description: string; completed: boolean;}
type MyOmit<T, K extends keyof T> = { [Key in keyof T as Key extends K ? never : Key]: T[Key];};
type TodoPreview = MyOmit<Todo, "description" | "title">;// { completed: boolean }Mapped Type에서 as 절을 사용하면 키를 조건부로 필터링하거나 변환할 수 있습니다.
3. Readonly 2 (부분 Readonly)
섹션 제목: “3. Readonly 2 (부분 Readonly)”특정 키만 readonly로 만들고 나머지는 그대로 유지합니다.
type MyReadonly2<T, K extends keyof T> = { readonly [Key in K]: T[Key];} & { [Key in keyof T as Key extends K ? never : Key]: T[Key];};유틸리티 타입을 조합한 간결한 버전:
type MyReadonly2<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>;Medium부터는 이렇게 여러 유틸리티를 조합해야 하는 문제가 많습니다.
4. Deep Readonly
섹션 제목: “4. Deep Readonly”중첩된 객체의 모든 프로퍼티를 재귀적으로 readonly로 만듭니다.
type DeepReadonly<T> = { readonly [Key in keyof T]: DeepReadonly<T[Key]>;};
type X = { x: { a: 1; b: "hi" }; y: "hey";};
type Todo = DeepReadonly<X>;// { readonly x: { readonly a: 1; readonly b: "hi" }; readonly y: "hey" }5. Tuple to Union
섹션 제목: “5. Tuple to Union”튜플의 모든 요소를 유니온 타입으로 변환합니다.
type Arr = ["1", "2", "3"];
type TupleToUnion<T extends readonly any[]> = T[number];
type Test = TupleToUnion<Arr>; // "1" | "2" | "3"6. Chainable Options
섹션 제목: “6. Chainable Options”빌더 패턴처럼 메서드 체이닝으로 옵션을 추가하는 타입입니다. intersection을 이용해 타입을 점진적으로 축적합니다.
type Chainable<T = {}> = { option<U extends string, V>( key: U, value: V ): Chainable<T & { [key in U]: V }>; get(): T;};
declare const config: Chainable;const result = config .option("foo", 123) .option("name", "type-challenges") .option("bar", { value: "Hello World" }) .get();
// { foo: number; name: string; bar: { value: string } }7. Last of Array
섹션 제목: “7. Last of Array”배열의 마지막 요소 타입을 추출합니다. 스프레드 연산자가 타입에서도 앞쪽에 사용 가능하다는 점이 핵심입니다.
type Last<T extends readonly unknown[]> = T extends [...infer Rest, infer R] ? R : never;
type tail1 = Last<["a", "b", "c"]>; // "c"type tail2 = Last<[3, 2, 1]>; // 18. Pop
섹션 제목: “8. Pop”배열에서 마지막 요소를 제거한 타입을 반환합니다. Last와 같은 패턴에서 Rest를 반환합니다.
type Pop<T extends readonly unknown[]> = T extends [...infer Rest, infer R] ? Rest : never;
type re1 = Pop<["a", "b", "c", "d"]>; // ["a", "b", "c"]type re2 = Pop<[3, 2, 1]>; // [3, 2]9. Promise.all 타입
섹션 제목: “9. Promise.all 타입”Promise.all의 반환 타입을 구현합니다. 튜플의 각 요소가 Promise이면 풀고, 아니면 그대로 둡니다.
const PromiseAll = <T extends readonly unknown[]>( arr: T): Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];}> => { return Promise.all(arr.map((item) => Promise.resolve(item))) as any;};
const promise1 = Promise.resolve(3);const promise2 = 42;const promise3 = new Promise<string>((resolve) => { setTimeout(resolve, 100, "foo");});
// Promise<[number, 42, string]>const p = PromiseAll([promise1, promise2, promise3] as const);10. Type Lookup
섹션 제목: “10. Type Lookup”디스크리미네이티드 유니온에서 type 필드를 기준으로 특정 타입을 추출합니다.
interface Cat { type: "cat"; breeds: "Abyssinian" | "Shorthair" | "Curl" | "Bengal";}
interface Dog { type: "dog"; breeds: "Hound" | "Brittany" | "Bulldog" | "Boxer"; color: "brown" | "white" | "black";}
type LookUp<T extends { type: string }, K extends T["type"]> = T extends { type: K;} ? T : never;
type MyDogType = LookUp<Cat | Dog, "dog">; // Dog타입을 집합으로 봐야 이해가 됩니다. T extends { type: K }는 type 필드가 K인 멤버만 필터링합니다.
11. Trim Left
섹션 제목: “11. Trim Left”문자열 앞의 공백을 재귀적으로 제거합니다.
type Space = " " | "\n" | "\t";
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S;
type trimmed = TrimLeft<" Hello World ">; // "Hello World "12. Trim
섹션 제목: “12. Trim”양쪽 공백을 모두 제거합니다. TrimLeft와 TrimRight를 조합하거나, 유니온으로 한 번에 처리할 수 있습니다.
type Space = " " | "\n" | "\t";
// 방법 1: 각각 구현 후 조합type TrimLeft<T extends string> = T extends `${Space}${infer R}` ? TrimLeft<R> : T;type TrimRight<U extends string> = U extends `${infer R}${Space}` ? TrimRight<R> : U;type Trim<V extends string> = TrimLeft<TrimRight<V>>;
// 방법 2: 유니온으로 한 번에type Trim2<T extends string> = T extends | `${Space}${infer R}` | `${infer R}${Space}` ? Trim2<R> : T;
type trimmed = Trim<" Hello World ">; // "Hello World"13. Capitalize
섹션 제목: “13. Capitalize”문자열의 첫 글자를 대문자로 변환합니다.
type Capitalize1<S extends string> = S extends `${infer R}${infer Tail}` ? `${Uppercase<R>}${Tail}` : S;
type capitalized = Capitalize1<"hello world">; // "Hello world"14. Replace
섹션 제목: “14. Replace”문자열에서 첫 번째로 매칭되는 부분을 교체합니다.
type Replace< T extends string, From extends string, To extends string> = T extends `${infer R}${From}${infer S}` ? `${R}${To}${S}` : T;
type replaced = Replace<"types are fun!", "fun", "awesome">;// "types are awesome!"15. ReplaceAll
섹션 제목: “15. ReplaceAll”모든 매칭을 재귀적으로 교체합니다. Replace를 재귀로 확장합니다.
type ReplaceAll< T extends string, U extends string, V extends string> = T extends `${infer R}${U}${infer S}` ? ReplaceAll<`${R}${V}${S}`, U, V> : T;
type replaced = ReplaceAll<"t y p e s", " ", "">; // "types"16. Append Argument
섹션 제목: “16. Append Argument”함수에 새로운 매개변수를 추가합니다. infer를 두 번 사용해 기존 인자와 반환 타입을 동시에 추론합니다.
type Fn = (a: number, b: string) => number;
type AppendArgument<T extends (...args: any[]) => unknown, U> = T extends ( ...args: infer R) => infer S ? (...args: [...R, U]) => S : never;
type Result = AppendArgument<Fn, boolean>;// (a: number, b: string, args_2: boolean) => number17. Permutation
섹션 제목: “17. Permutation”유니온의 모든 순열을 튜플로 생성합니다. 분산 조건부 타입의 핵심 활용 사례입니다.
type Permutation<T, K = T> = [T] extends [never] ? [] : K extends K ? [K, ...Permutation<Exclude<T, K>>] : never;
type perm = Permutation<"A" | "B" | "C">;// ["A","B","C"] | ["A","C","B"] | ["B","A","C"] | ["B","C","A"] | ["C","A","B"] | ["C","B","A"]핵심 개념:
- 분산 조건부 타입:
K extends K에서K가 유니온이면 각 멤버에 대해 개별 평가됩니다. <T, K = T>: 제네릭 기본값으로K를T와 동일하게 설정합니다.[T] extends [never]:T가never인지 확인합니다. 튜플로 감싸야 분산이 방지됩니다.
18. Flatten
섹션 제목: “18. Flatten”중첩된 배열을 재귀적으로 평탄화합니다. 재귀 타입의 대표적인 활용 예시입니다.
type Flatten<S extends any[], T extends any[] = []> = S extends [ infer X, ...infer Y,] ? X extends any[] ? Flatten<[...X, ...Y], T> : Flatten<[...Y], [...T, X]> : T;
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]>; // [1, 2, 3, 4, 5]동작 과정:
Flatten<[1, 2, [3, 4], [[[5]]]], []>Flatten<[2, [3, 4], [[[5]]]], [1]>Flatten<[[3, 4], [[[5]]]], [1, 2]>Flatten<[3, 4, [[[5]]]], [1, 2]>(배열이므로 펼침)Flatten<[4, [[[5]]]], [1, 2, 3]>Flatten<[[[[5]]]], [1, 2, 3, 4]>Flatten<[[[5]]], [1, 2, 3, 4]>→ … →[1, 2, 3, 4, 5]
19. Append to Object
섹션 제목: “19. Append to Object”객체에 새로운 프로퍼티를 추가합니다.
type Test = { id: "1" };
type AppendToObject< T, K extends string | number, Value> = T extends object ? { [Key in keyof T | K]: Key extends keyof T ? T[Key] : Value; } : never;
type Result = AppendToObject<Test, "value", 4>;// { id: "1"; value: 4 }keyof T | K로 기존 키와 새로운 키를 합치고, 조건부 타입으로 각 키의 값 타입을 결정합니다.