콘텐츠로 이동

타입 챌린지 Medium 풀이

Medium 난이도부터는 여러 타입 도구를 조합해야 합니다. 재귀 타입, 분산 조건부 타입, 템플릿 리터럴 타입 등 고급 패턴이 본격적으로 등장합니다.

함수의 반환 타입을 추출합니다. neverunknownany 대신 사용하는 것이 권장됩니다.

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 | 2

지정한 키를 제외한 나머지 프로퍼티를 가진 타입을 만듭니다. 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 절을 사용하면 키를 조건부로 필터링하거나 변환할 수 있습니다.

특정 키만 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부터는 이렇게 여러 유틸리티를 조합해야 하는 문제가 많습니다.

중첩된 객체의 모든 프로퍼티를 재귀적으로 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" }

튜플의 모든 요소를 유니온 타입으로 변환합니다.

type Arr = ["1", "2", "3"];
type TupleToUnion<T extends readonly any[]> = T[number];
type Test = TupleToUnion<Arr>; // "1" | "2" | "3"

빌더 패턴처럼 메서드 체이닝으로 옵션을 추가하는 타입입니다. 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 } }

배열의 마지막 요소 타입을 추출합니다. 스프레드 연산자가 타입에서도 앞쪽에 사용 가능하다는 점이 핵심입니다.

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]>; // 1

배열에서 마지막 요소를 제거한 타입을 반환합니다. 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]

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);

디스크리미네이티드 유니온에서 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인 멤버만 필터링합니다.

문자열 앞의 공백을 재귀적으로 제거합니다.

type Space = " " | "\n" | "\t";
type TrimLeft<S extends string> = S extends `${Space}${infer R}`
? TrimLeft<R>
: S;
type trimmed = TrimLeft<" Hello World ">; // "Hello World "

양쪽 공백을 모두 제거합니다. 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"

문자열의 첫 글자를 대문자로 변환합니다.

type Capitalize1<S extends string> = S extends `${infer R}${infer Tail}`
? `${Uppercase<R>}${Tail}`
: S;
type capitalized = Capitalize1<"hello world">; // "Hello world"

문자열에서 첫 번째로 매칭되는 부분을 교체합니다.

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!"

모든 매칭을 재귀적으로 교체합니다. 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"

함수에 새로운 매개변수를 추가합니다. 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) => number

유니온의 모든 순열을 튜플로 생성합니다. 분산 조건부 타입의 핵심 활용 사례입니다.

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>: 제네릭 기본값으로 KT와 동일하게 설정합니다.
  • [T] extends [never]: Tnever인지 확인합니다. 튜플로 감싸야 분산이 방지됩니다.

중첩된 배열을 재귀적으로 평탄화합니다. 재귀 타입의 대표적인 활용 예시입니다.

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]

동작 과정:

  1. Flatten<[1, 2, [3, 4], [[[5]]]], []>
  2. Flatten<[2, [3, 4], [[[5]]]], [1]>
  3. Flatten<[[3, 4], [[[5]]]], [1, 2]>
  4. Flatten<[3, 4, [[[5]]]], [1, 2]> (배열이므로 펼침)
  5. Flatten<[4, [[[5]]]], [1, 2, 3]>
  6. Flatten<[[[[5]]]], [1, 2, 3, 4]>
  7. Flatten<[[[5]]], [1, 2, 3, 4]> → … → [1, 2, 3, 4, 5]

객체에 새로운 프로퍼티를 추가합니다.

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로 기존 키와 새로운 키를 합치고, 조건부 타입으로 각 키의 값 타입을 결정합니다.