type-challenges는 TypeScript 타입 시스템을 퍼즐처럼 풀어보는 오픈소스 프로젝트입니다. 면접 준비에도 매우 좋습니다.

Easy 레벨

Pick 직접 구현

TYPESCRIPT
// 도전: 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 직접 구현

TYPESCRIPT
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

First 직접 구현

TYPESCRIPT
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

type A = First<[3, 2, 1]>; // 3
type B = First<[]>;         // never

Includes 구현

TYPESCRIPT
// 배열에 특정 타입이 포함되어 있는지 검사
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 직접 구현

TYPESCRIPT
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

// as를 사용해서 키를 필터링하는 게 핵심

DeepReadonly 구현

TYPESCRIPT
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

Flatten 구현

TYPESCRIPT
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 구현

TYPESCRIPT
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 구현

TYPESCRIPT
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 구현

TYPESCRIPT
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 구현

TYPESCRIPT
// '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 분리

TYPESCRIPT
// 리스트를 처리할 때는 항상 첫 번째 + 나머지로 분리
T extends [infer H, ...infer R]
  ? /* H로 처리 */ : /* 기저 조건 */

2. 문자열은 템플릿 리터럴로 분해

TYPESCRIPT
// 문자열을 처리할 때는 패턴 매칭
S extends `${infer Before}${Pattern}${infer After}`
  ? /* 매칭 성공 */ : /* 매칭 실패 */

3. 키 필터링은 as + never

TYPESCRIPT
// 특정 키를 제거할 때
{ [K in keyof T as 조건 ? K : never]: T[K] }

4. 숫자 연산은 튜플 길이

TYPESCRIPT
// 숫자를 다룰 때는 튜플로 변환
[...BuildTuple<A>, ...BuildTuple<B>]['length']

정리

  • type-challenges는 TypeScript 타입 테크닉을 실전적으로 연습하는 프로젝트다
  • Easy: Pick, Readonly, First 등 기본 유틸리티 구현
  • Medium: Omit, Flatten, Trim 등 복잡한 조합
  • Hard: CamelCase 변환, Query String 파싱 등
  • 핵심 패턴: 재귀 + infer + 조건부 타입 + as 절
  • 면접에서 "MyPick을 구현하세요" 같은 문제가 자주 나온다
댓글 로딩 중...