타입 챌린지로 배우는 고급 타입 — Easy부터 Hard까지 풀이
type-challenges는 TypeScript 타입 시스템을 퍼즐처럼 풀어보는 오픈소스 프로젝트입니다. 면접 준비에도 매우 좋습니다.
Easy 레벨
Pick 직접 구현
// 도전: MyPick<T, K>를 구현하세요
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 검증
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>;
// { title: string; completed: boolean }
Readonly 직접 구현
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
First 직접 구현
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type A = First<[3, 2, 1]>; // 3
type B = First<[]>; // never
Includes 구현
// 배열에 특정 타입이 포함되어 있는지 검사
type Includes<T extends readonly any[], U> =
T extends [infer H, ...infer R]
? Equal<H, U> extends true
? true
: Includes<R, U>
: false;
// Equal 헬퍼 (정확한 타입 비교)
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false;
Medium 레벨
Omit 직접 구현
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// as를 사용해서 키를 필터링하는 게 핵심
DeepReadonly 구현
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
Flatten 구현
type Flatten<T extends any[]> =
T extends [infer H, ...infer R]
? H extends any[]
? [...Flatten<H>, ...Flatten<R>]
: [H, ...Flatten<R>]
: [];
type Result = Flatten<[1, [2, [3, 4]], 5]>; // [1, 2, 3, 4, 5]
Trim 구현
type Whitespace = ' ' | '\n' | '\t';
type TrimLeft<S extends string> =
S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S;
type TrimRight<S extends string> =
S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
type Result = Trim<' hello '>; // 'hello'
Replace 구현
type Replace<
S extends string,
From extends string,
To extends string
> = From extends ''
? S
: S extends `${infer Before}${From}${infer After}`
? `${Before}${To}${After}`
: S;
type Result = Replace<'hello world', 'world', 'TS'>; // 'hello TS'
Hard 레벨
CamelCase 구현
type CamelCase<S extends string> =
S extends `${infer H}-${infer R}`
? R extends Capitalize<R>
? `${H}-${CamelCase<R>}`
: `${H}${CamelCase<Capitalize<R>>}`
: S;
type Result = CamelCase<'foo-bar-baz'>; // 'fooBarBaz'
ParseQueryString 구현
// 'key1=value1&key2=value2'를 파싱하는 타입
type ParseQueryString<S extends string> =
S extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key]: Value } & ParseQueryString<Rest>
: S extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type Params = ParseQueryString<'name=홍길동&age=25'>;
// { name: '홍길동' } & { age: '25' }
풀이 팁
공부하다 보니 타입 챌린지를 풀 때 몇 가지 패턴이 반복되더라고요.
1. 재귀는 Head + Tail 분리
// 리스트를 처리할 때는 항상 첫 번째 + 나머지로 분리
T extends [infer H, ...infer R]
? /* H로 처리 */ : /* 기저 조건 */
2. 문자열은 템플릿 리터럴로 분해
// 문자열을 처리할 때는 패턴 매칭
S extends `${infer Before}${Pattern}${infer After}`
? /* 매칭 성공 */ : /* 매칭 실패 */
3. 키 필터링은 as + never
// 특정 키를 제거할 때
{ [K in keyof T as 조건 ? K : never]: T[K] }
4. 숫자 연산은 튜플 길이
// 숫자를 다룰 때는 튜플로 변환
[...BuildTuple<A>, ...BuildTuple<B>]['length']
정리
- type-challenges는 TypeScript 타입 테크닉을 실전적으로 연습하는 프로젝트다
- Easy: Pick, Readonly, First 등 기본 유틸리티 구현
- Medium: Omit, Flatten, Trim 등 복잡한 조합
- Hard: CamelCase 변환, Query String 파싱 등
- 핵심 패턴: 재귀 + infer + 조건부 타입 + as 절
- 면접에서 "MyPick을 구현하세요" 같은 문제가 자주 나온다
댓글 로딩 중...