🔥 값 레벨에서 타입 레벨까지

654자
7분

TypeScript 입문에서 타입 레벨 프로그래밍까지의 학습 여정 표지

TypeScript 는 무엇인가

TypeScript 를 처음 만졌을 때 가장 먼저 본 건 에디터의 빨간 줄이었다. add(3, "4") 처럼 string 을 number 매개변수에 넣자 Argument of type 'string' is not assignable to parameter of type 'number' 라는 에러가 떴고, 뜻은 'number 매개변수에 string 타입 값을 넣을 수 없다' 였다. JavaScript 라면 실행해야 알았을 실수가 코드를 적는 시점에 멈췄다.

TypeScript 는 JavaScript 위에 타입 검사 한 겹을 더 얹는 언어다. 변수와 매개변수에 타입을 달면, 컴파일 시점에 컴파일러가 그 타입의 일관성을 검사한다.

ts
// file: typed.ts
export {};
 
function add(a: number, b: number): number {
  return a + b;
}
 
console.log(add(3, 4));  // 7
// 아래 줄의 주석을 풀면 TS2345 컴파일 에러:
// console.log(add("3", 4));
// Argument of type 'string' is not assignable to parameter of type 'number'.
ts
// file: typed.ts
export {};
 
function add(a: number, b: number): number {
  return a + b;
}
 
console.log(add(3, 4));  // 7
// 아래 줄의 주석을 풀면 TS2345 컴파일 에러:
// console.log(add("3", 4));
// Argument of type 'string' is not assignable to parameter of type 'number'.

이런 컴파일 시점 안전망 때문에 새 프로젝트는 대부분 TypeScript 로 시작한다. 기존 JavaScript 코드베이스도 점진적으로 TypeScript 로 옮겨가는 흐름이다.

기본 사용법까지는 무난하다. 변수에 타입을 달고, 인터페이스를 정의하고, 제네릭 함수를 짜는 단계는 입문 자료가 충분히 안내한다. 진짜 어려운 부분은 다른 곳에 있다.

진짜 어려운 부분

type-fest 나 react-router 같은 인기 라이브러리의 타입 정의를 열면 입문 자료에서 보지 못한 문법이 한꺼번에 등장한다.

ts
// file: deep-readonly.ts
export {};
 
// 깊이 모든 속성을 readonly 로 만드는 유틸리티 타입
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;
 
type Config = {
  server: { port: number; host: string };
  features: { auth: boolean };
};
 
type FrozenConfig = DeepReadonly<Config>;
// 결과: 모든 중첩 속성이 readonly 로 마킹됨
ts
// file: deep-readonly.ts
export {};
 
// 깊이 모든 속성을 readonly 로 만드는 유틸리티 타입
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;
 
type Config = {
  server: { port: number; host: string };
  features: { auth: boolean };
};
 
type FrozenConfig = DeepReadonly<Config>;
// 결과: 모든 중첩 속성이 readonly 로 마킹됨

extends, 매핑 타입, 재귀가 한 줄에 같이 들어 있어서 처음 읽으면 왜 이렇게 적는지 바로 설명하기 어렵다. 그래서 바로 다음 예시까지 이어서 봐야 공통 규칙을 분해할 수 있다.

react-router 처럼 라우트 문자열에서 파라미터 키를 추출하는 라이브러리는 한 단계 더 어려운 타입을 짠다.

ts
// file: route-params.ts
export {};
 
// "/users/:id/posts/:postId" 같은 라우트에서 파라미터 키를 추출하는 타입
type RouteParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & RouteParams<Rest>
    : S extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};
 
type Params = RouteParams<"/users/:id/posts/:postId">;
// 결과: { id: string } & { postId: string }
ts
// file: route-params.ts
export {};
 
// "/users/:id/posts/:postId" 같은 라우트에서 파라미터 키를 추출하는 타입
type RouteParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & RouteParams<Rest>
    : S extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};
 
type Params = RouteParams<"/users/:id/posts/:postId">;
// 결과: { id: string } & { postId: string }

infer, 템플릿 리터럴 타입, 분배 조건부, 재귀가 함께 들어 있으니 type-fest 와 react-router 같은 라이브러리들은 타입 레벨에서도 작은 프로그램 을 짠다. 그래서 TypeScript 의 진짜 어려움은 타입 시스템 자체를 하나의 작은 언어로 읽어야 한다는 데 있다.

두 세계: 아래는 값 레벨의 중괄호와 괄호, 위는 타입 레벨의 꺾쇠와 타입 기호. 값 레벨에서 타입 레벨로 올라가는 화살표가 둘을 잇는다

두 레벨을 한 흐름으로

값 레벨에서 타입 레벨까지, 타입 시스템을 하나의 언어로 다루는 단계까지 한 흐름으로 짠다.

값 레벨에서는 타입 주석, 추론, 객체와 인터페이스, 유니온과 좁히기, 함수와 클래스, 제네릭, 유틸리티 타입을 다룬다. 실무 코드에서 매일 쓰는 문법과 기능이다.

타입 레벨에서는 조건부 타입, 매핑 타입, 템플릿 리터럴 타입, 재귀, infer 의 패턴 매칭을 다룬다. 그리고 그것을 도메인 모델링과 라이브러리 코드 분석에 적용한다. 위에서 본 DeepReadonlyRouteParams 같은 코드가 어떻게 동작하는지 직접 분해할 수 있는 단계까지 간다.

같은 개념도 단계마다 맡는 역할이 다르다. 제네릭은 처음에는 함수 추상화 도구로 다루고, 뒤에서는 타입을 계산하는 도구로 다시 쓴다.

안쪽 원의 값 레벨에서 다룬 개념이 바깥쪽 원의 타입 레벨에서 다시 다뤄진다는 동심원 구조

TS 6.0 이 기준

이 글은 TypeScript 6.0 을 기준으로 적는다. 2026 년 3 월에 나온 버전이고, 본문 코드는 모두 6.0 에서 그대로 동작한다. 5.x 와 비교하면 기본값 몇 개가 달라져서 설정 설명도 그 기준에 맞춰 읽어야 한다.

  • strict: true 가 기본이다. 명시할 필요가 없다.
  • target: "es2025" 가 권장이다. ES5 타깃은 deprecate 됐다.
  • module: "nodenext" + moduleResolution: "nodenext" 가 표준 조합이다.
  • verbatimModuleSyntax: true 로 컴파일러가 import type 와 일반 import 를 분명히 구분하도록 강제한다.
  • TypeScript 6.0 에서는 --outFile, --baseUrl 같은 레거시 옵션을 더 이상 쓰지 않는다.

라이브러리 코드 안에는 입문 자료에서 보지 못한 타입 레벨 프로그래밍이 들어 있다. 타입 레벨까지 가려면 값 레벨부터 차근차근 짚어야 한다. 값 레벨에서 익힌 제네릭 같은 도구를 타입 레벨에서도 그대로 다시 쓴다.

YouTube 영상

채널 보기
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기
투영과 예측, 그리고 선형 결합 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
직교성과 벡터 투영 | 선형대수학
AI 추천 시스템의 원리, 벡터 사이의 각도와 코사인 유사도 | 선형대수학
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학