✍️ 필사 모드: TypeScript 타입 시스템의 깊이 — 구조적 타이핑, Generic, Conditional, satisfies, tsc Go 포팅, Zod, tRPC까지 (2025)
한국어"TypeScript was a humble goal: add types to JavaScript. Ten years later, it's become one of the most sophisticated practical type systems ever shipped." — Anders Hejlsberg (C# 수석 아키텍트, TypeScript 창시자)
2012년 10월 1일, Microsoft가 "JavaScript for Application-scale Development"라는 이름으로 TypeScript를 공개했다. 당시 반응은 미지근했다. "또 Microsoft가 자바스크립트를 지배하려는 시도"라는 냉소가 많았다.
13년이 지난 2025년, Stack Overflow 조사에서 TypeScript가 가장 사랑받는 언어 3위다. React, Vue, Angular, Next.js 등 주요 프레임워크가 기본 언어로 채택했고, GitHub 신규 JS 리포의 70%가 TypeScript로 시작한다. Deno와 Bun은 타입스크립트를 네이티브 실행한다.
이 글은 ".ts 파일을 쓴다"에서 "TypeScript의 깊이를 이해한다"로 넘어가려는 사람을 위한 지도다.
1. Gradual Typing — 성공의 공식
왜 다른 시도들은 실패했나
- CoffeeScript (2009) — 더 나은 문법, 하지만 타입 없음
- Dart (2011) — Google, 독자 런타임 요구로 실패
- Flow (2014) — Facebook, 타입만 추가했지만 TS와 경쟁 실패
- Nim, ReasonML — 니치에 머무름
TypeScript가 살아남은 이유:
- 점진적 도입 — 한 파일씩 마이그레이션 가능
- JavaScript 슈퍼셋 — 모든 JS가 유효한 TS
- 컴파일 후 순수 JS — 런타임 의존성 제로
- 강력한 추론 — 타입 애너테이션 최소화
- 거대한 커뮤니티 — DefinitelyTyped 17만+ 패키지
"Soundness를 포기했기에 성공했다"
- TypeScript는 **soundness(완전성)**를 의도적으로 포기
any, 타입 단언(as), 교차 타입 허용- 그 결과: 실용적
- Flow는 더 엄격했지만 실용성에서 밀림
2. 구조적 타이핑 (Structural Typing)
Nominal vs Structural
Nominal (Java, C#): 타입의 "이름"이 일치해야
class Point { int x, y; }
class Vec2 { int x, y; }
Point p = new Vec2(); // 에러! 다른 클래스
Structural (TypeScript, Go): 모양(shape)이 일치하면 OK
interface Point { x: number; y: number }
interface Vec2 { x: number; y: number }
const p: Point = { x: 1, y: 2 } // OK
const v: Vec2 = p // OK, 같은 모양
Duck Typing의 정적 버전
"오리처럼 걷고 오리처럼 울면 오리다" — 런타임에 검사하는 파이썬의 방식을 컴파일 시간에 한다.
장점
- 유연성 — 라이브러리 간 타입 호환 쉬움
- 리팩토링 — interface 이름 바꿔도 영향 없음
- Mock 테스트 편함 — 구조만 맞으면 OK
단점 — Branded Types의 필요
type UserId = string
type PostId = string
function getUser(id: UserId) { ... }
const postId: PostId = 'p123'
getUser(postId) // 구조가 같아서 OK... 버그!
해결:
type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }
const userId = 'u1' as UserId
const postId = 'p1' as PostId
getUser(postId) // 에러!
2024년 TypeScript 5.6에서 Branded Types를 공식 지원 패턴으로 문서화.
3. 추론의 예술 — Type Inference
리터럴 추론
let x = 'hello' // string
const y = 'hello' // "hello" (리터럴)
const z = { a: 1 } // { a: number } -- readonly 아님
const는 값 자체를 타입으로, let은 넓힌 타입.
as const — 완전 불변
const config = {
mode: 'production',
debug: false,
} as const
// { readonly mode: "production"; readonly debug: false }
함수 추론
function map<T, U>(arr: T[], fn: (x: T) => U): U[] { ... }
const nums = [1, 2, 3]
map(nums, x => x * 2) // T=number, U=number 자동 추론
제약 있는 추론
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Kim', age: 20 }
pick(user, 'name') // string (T[K] = T['name'] = string)
4. Generic — 함수의 추상화
기본 제네릭
function identity<T>(x: T): T { return x }
const n = identity(42) // number
const s = identity('hello') // string
제약 (Constraint)
interface HasLength { length: number }
function logLength<T extends HasLength>(x: T): T {
console.log(x.length)
return x
}
logLength('hello') // OK
logLength([1,2,3]) // OK
logLength(123) // 에러 (length 없음)
기본값
type Page<T = any> = {
items: T[]
total: number
}
type StringPage = Page<string>
type DefaultPage = Page // Page<any>
Variance — 공변성과 반공변성
type Printer<T> = (x: T) => void
let animalPrinter: Printer<Animal>
let dogPrinter: Printer<Dog>
animalPrinter = dogPrinter // 에러 (Printer는 반공변)
dogPrinter = animalPrinter // OK
함수 인자는 반공변, 리턴은 공변. TypeScript는 기본 이변(bivariant)지만 strictFunctionTypes에서 올바름.
5. Conditional Types — 타입 수준의 if
기본 문법
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
infer — 타입 추출
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() { return { id: 1, name: 'Kim' } }
type User = ReturnType<typeof getUser> // { id: number; name: string }
TypeScript 표준 ReturnType, Parameters, Awaited 모두 이 메커니즘.
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never
type A = ToArray<string | number>
// string[] | number[] (Union에 분배됨)
Union type이 자동으로 순회됨.
실전 — 유용한 조합
// Truthy만 추출
type NonNullable<T> = T extends null | undefined ? never : T
// 함수가 아닌 것만
type NonFunction<T> = T extends Function ? never : T
// Promise unwrap
type Unwrap<T> = T extends Promise<infer U> ? U : T
6. Mapped Types — 객체 변환의 힘
기본
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
type User = { name: string; age: number }
type ReadonlyUser = Readonly<User>
// { readonly name: string; readonly age: number }
변경자 — +?, -?, +readonly, -readonly
type Required<T> = { [K in keyof T]-?: T[K] } // ? 제거
type Mutable<T> = { -readonly [K in keyof T]: T[K] } // readonly 제거
Key Remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}
type User = { name: string; age: number }
type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }
실전 — DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
재귀 + 조건부 + 맵드 타입의 조합.
7. Template Literal Types (TS 4.1, 2020)
기본
type Greeting = `Hello, ${string}`
const g: Greeting = 'Hello, World' // OK
const bad: Greeting = 'Hi, World' // 에러
조합
type Color = 'red' | 'blue'
type Size = 'sm' | 'lg'
type Class = `${Color}-${Size}`
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"
파싱 — infer 와 결합
type ExtractRoute<T> =
T extends `/users/${infer Id}/posts/${infer PostId}`
? { id: Id; postId: PostId }
: never
type Route = ExtractRoute<'/users/123/posts/456'>
// { id: "123"; postId: "456" }
이 패턴으로 Router 타입 안전성(Next.js, Remix가 내부적으로 활용).
유명한 활용 — Tailwind IntelliSense
text-red-500, bg-blue-200 같은 수천 개 클래스를 타입으로 표현. Template literal types가 없었으면 불가능.
8. satisfies — 2022년의 작은 혁명
문제
type Config = { [key: string]: string | number }
const config = {
name: 'app',
port: 3000,
} as Config
config.name.toUpperCase() // 에러! string | number라서
as로 캐스팅하면 구체 타입 정보를 잃음.
satisfies 해결 (TS 4.9)
const config = {
name: 'app',
port: 3000,
} satisfies Config
config.name.toUpperCase() // OK, string으로 추론
config.port.toFixed(2) // OK, number로 추론
"이 타입을 만족하는지 검사하되, 구체 타입은 유지하라."
실전 예
type Themes = Record<string, { primary: string; secondary: string }>
const themes = {
light: { primary: '#fff', secondary: '#000' },
dark: { primary: '#000', secondary: '#fff' },
} satisfies Themes
// themes.light.primary -- "#fff" (리터럴 타입 유지!)
// themes.unknown -- 에러 (정의 안 됨)
as Themes로 하면 string이 되지만, satisfies로는 리터럴을 유지. 상수 객체에서 특히 유용.
9. 타입 수준 프로그래밍 — Type Challenges
GitHub의 type-challenges/type-challenges는 TypeScript 타입만으로 풀어야 하는 문제 모음. 예:
구현: Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
구현: DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
}
구현: TupleToObject
type TupleToObject<T extends readonly string[]> = {
[K in T[number]]: K
}
type Result = TupleToObject<['a', 'b']>
// { a: 'a'; b: 'b' }
왜 가치 있는가
- 타입 시스템의 한계와 능력 이해
- 라이브러리 작성자 수준의 타입 설계
- 뇌의 근력 트레이닝
단, 프로덕션에서 과도한 타입 프로그래밍은 피하라 — 컴파일 속도와 가독성 저하.
10. 모듈 해석의 복잡성
CommonJS vs ESM
// CommonJS
const mod = require('./foo')
module.exports = { ... }
// ESM
import mod from './foo'
export default { ... }
Node.js가 두 시스템을 모두 지원하며, 이를 TypeScript가 중개하는 것은 지옥이다.
module 옵션
commonjs— 전통적esnext— 최신 ESMnode16/nodenext— Node 16+의 ESM 인식preserve— 그대로 유지
moduleResolution
node— 전통 Node 방식node16/nodenext—exports필드,.mjs/.cjs인식bundler(TS 5.0+) — Vite/esbuild 등 번들러 가정
2024년 현실
- 라이브러리 저자는 dual package(CJS + ESM) 출시 부담
exports필드로 조건부 해결- ESM-only로 넘어가는 추세 (Vite, Vitest, Remix)
11. tsc의 한계와 Go 포팅 (2025)
tsc의 문제
- TypeScript로 작성됨 (dogfooding)
- 대형 프로젝트: 컴파일 30초-3분
- LSP 응답: 1-2초
- 개발 경험 병목
대안들
- swc — Rust 기반, ~20배 빠름, 타입 체크 안 함
- esbuild — Go, 100배 빠름, 타입 체크 안 함
- Bun — Zig, 네이티브 실행
- tsgo / @typescript/compiler — 실험적
Anders의 충격 발표 (2025년 3월)
"We are porting the TypeScript compiler to Go."
- Anders Hejlsberg 직접 주도
- 10배 빠른 컴파일, 8배 빠른 LSP
- 병렬화 가능 (JS 단일 스레드 벗어남)
- 호환성 100% 유지
- 2026-2027년 일반 공개 목표
Why Go, Not Rust?
- "동등한 성능, 낮은 학습 곡선, 큰 표준 라이브러리"
- GC 있음 — 기존 아키텍처 이식 쉬움
- Rust는 "훌륭하지만 2년 더 걸릴 것"
의미
- 10년 만에 tsc의 가장 큰 변화
- 거대 모노레포(Slack, Airbnb, Microsoft)의 DX 혁명
- 2027년 기준 TypeScript 개발이 C# 수준으로 빨라질 것
12. 런타임 검증의 진화 — Zod, ArkType, Valibot
문제 — 타입은 컴파일 시간만
function parseUser(json: string): User {
return JSON.parse(json) // 런타임에 타입 검증 없음!
}
외부 데이터(API, 폼, 파일)는 런타임 검증 필요.
Zod — 2020년 등장, 현재 표준
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
age: z.number().int().positive(),
})
type User = z.infer<typeof UserSchema> // 타입 자동 추출!
const user = UserSchema.parse(externalData) // 검증 실패 시 throw
- 스키마 하나로 타입 + 검증
- 강력한 조합자 (transform, refine, union)
- React Hook Form, tRPC 등 표준 통합
ArkType — 타입 그대로 검증
import { type } from 'arktype'
const User = type({
id: 'number',
email: 'email',
age: 'number > 0',
})
TypeScript 문법 그대로 (문자열 DSL). 엄청 빠름.
Valibot — Zod의 트리쉐이킹 개선
import { object, string, email, number, minValue, pipe } from 'valibot'
const UserSchema = object({
email: pipe(string(), email()),
age: pipe(number(), minValue(0)),
})
개별 함수 import → 번들 크기 70% 감소. Zod의 강력한 라이벌.
비교
| 측면 | Zod | ArkType | Valibot |
|---|---|---|---|
| 성숙도 | 높음 | 중간 | 중간 |
| 번들 크기 | 큼 | 중간 | 최소 |
| 성능 | 중간 | 최고 | 빠름 |
| 문법 | 메서드 체인 | TS DSL | 함수형 |
| 생태계 | 최대 | 성장중 | 성장중 |
13. tRPC — End-to-End 타입 안전성
아이디어
클라이언트 ──(타입 공유)── 서버
GraphQL 없이도 프론트엔드가 백엔드 타입을 그대로 사용.
서버 정의
// server/router.ts
import { z } from 'zod'
import { router, publicProcedure } from './trpc'
export const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await db.users.find(input.id)
}),
})
export type AppRouter = typeof appRouter
클라이언트 사용
// client.ts
import { createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '../server/router'
const trpc = createTRPCProxyClient<AppRouter>({ ... })
const user = await trpc.getUser.query({ id: 1 })
// user의 타입이 자동 추론! (서버 DB 쿼리 리턴 타입)
마법
- 런타임에 HTTP 호출
- 타입은 빌드 시 공유 (타입만 import)
id의 타입이 틀리면 컴파일 에러- GraphQL의 80% 장점, 0% 복잡성
채택
- Next.js 풀스택 프로젝트에 압도적
- Theo(t3-stack) 주도
- 2024년부터 Remix, SvelteKit 통합
14. Effect-TS — 함수형 타입의 프론티어
아이디어
Haskell의 Effect system을 TypeScript로. 부수 효과, 에러, 의존성을 타입으로 관리.
import { Effect } from 'effect'
const getUser = (id: number): Effect.Effect<User, DbError, DbService> =>
Effect.gen(function*() {
const db = yield* DbService
return yield* db.findUser(id)
})
타입 의미:
- 성공 타입: User
- 에러 타입: DbError (체크드 예외처럼)
- 요구 서비스: DbService (DI)
장점
- 예외가 타입에 보임 — catch 누락 방지
- 의존성 주입 자동
- 리트라이, 타임아웃, 캐시가 합성 가능
단점
- 학습 곡선 가파름
- 생태계 작음
- 팀 전체 동의 필요
2024-2025 확산
- Discord, Vercel 팀 일부 적용
- Effect Conference 개최
- 함수형 TS의 사실상 표준화
15. 2025년 TypeScript 배우는 순서
초급
- 기본 타입 (
string,number,boolean,string[],Record) - Interface / Type alias
- Union / Intersection
- Generic 기초
- Utility types (
Partial,Pick,Omit)
중급
- Conditional Types
- Mapped Types
keyof,typeof,in- Template Literal Types
satisfies활용- Zod로 런타임 검증
고급
infer고급 활용- Recursive types
- Variance와 함수 타입
- 타입 디버깅 (
$ExpectType) - 라이브러리 수준 타입 설계
전문
- Effect-TS
- tRPC 풀스택
- Type Challenges
- TypeScript 컴파일러 API
16. 안티패턴 TOP 10
any남용 — 타입 시스템 무효화// @ts-ignore방치 — 버그 숨기는 습관as강제 캐스팅 —satisfies가 거의 항상 낫다- Enum 사용 —
as constunion이 더 안전 Function타입 — 구체적 시그너처 작성- 과도한 타입 프로그래밍 — 컴파일 느려짐, 가독성 ↓
- 타입과 런타임 불일치 — Zod 등으로 검증
readonly누락 — 불변 의도를 타입에 표현- Interface와 Type 규칙 없이 혼용 — 팀 컨벤션 필요
@types/*버전 불일치 — 런타임 버전과 맞추기
17. TypeScript 체크리스트
- strict: true —
strictNullChecks,noImplicitAny등 - noUncheckedIndexedAccess — 배열 인덱싱 안전
- 런타임 검증 — Zod/ArkType/Valibot 도입
-
as최소화,satisfies우선 - Branded Types — 구조적 타이핑 함정 방어
- tsconfig paths — alias로 import 정리
- moduleResolution: bundler (Vite/Next.js 사용 시)
-
isolatedModules— Vite/esbuild 호환 - CI에서
tsc --noEmit실행 -
typescript-eslint플러그인 활용 - Type coverage 측정 (
type-coverage패키지) - DefinitelyTyped 기여 경험 (오픈소스 기여)
마치며 — 언어가 아니라 도구
Anders Hejlsberg는 한 인터뷰에서 이렇게 말했다:
"TypeScript is not trying to be the perfect type system. It's trying to be the most useful type system for the largest number of JavaScript developers."
완벽한 타입 안전성(Haskell, Rust)과 완전한 자유(동적 언어) 사이에, TypeScript는 실용주의의 정점을 점하고 있다. 그 유연성 때문에 처음엔 어중간해 보이지만, 10년 축적된 결과는 대형 팀의 생산성을 가장 극적으로 끌어올린 도구다.
2025년의 TypeScript는:
- Go로 포팅되어 10배 빨라질 예정
- Zod 같은 런타임 검증과 타이트한 통합
- tRPC로 풀스택 타입 안전성 실현
- Effect-TS로 함수형 프런티어 탐험
언어의 혁명은 아직 끝나지 않았다.
다음 글 예고 — Rust와 시스템 프로그래밍의 귀환
TypeScript가 "가장 사랑받는 언어 3위"라면, Rust는 8년 연속 1위다. 왜? 다음 글에서는:
- Rust의 탄생 — 2006년 Graydon Hoare의 개인 프로젝트
- Ownership & Borrowing — 가비지 컬렉터 없는 메모리 안전
- Lifetime — 왜
'a가 어려운가 - Trait System — 인터페이스 + 제네릭의 우아한 결합
- Zero-cost abstractions — 고수준 문법 + C급 성능
- Async/Await와 Tokio — 런타임 생태계
- Cargo — 번들러의 이상향
- Rust가 정복한 분야 — 브라우저(Firefox), OS(Linux), 번들러(swc, Turbopack), 런타임(Deno)
- Rust로 쓰는 프론트엔드 — Leptos, Dioxus, Yew
- Why Rust는 안 죽는다 — Ferrocene, 정부 보안 권장
시스템 프로그래밍의 르네상스를 정리하는 여정.
"TypeScript is what JavaScript could have been, if JavaScript had been designed with tooling in mind." — Daniel Rosenwasser (TypeScript Program Manager)
현재 단락 (1/342)
2012년 10월 1일, Microsoft가 "JavaScript for Application-scale Development"라는 이름으로 TypeScript를 공개했다. 당시 ...