필사 모드: 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가 살아남은 이유:
1. **점진적 도입** — 한 파일씩 마이그레이션 가능
2. **JavaScript 슈퍼셋** — 모든 JS가 유효한 TS
3. **컴파일 후 순수 JS** — 런타임 의존성 제로
4. **강력한 추론** — 타입 애너테이션 최소화
5. **거대한 커뮤니티** — 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
export default { ... }
Node.js가 두 시스템을 모두 지원하며, 이를 TypeScript가 중개하는 것은 **지옥**이다.
`module` 옵션
- `commonjs` — 전통적
- `esnext` — 최신 ESM
- `node16` / `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년 등장, 현재 표준
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 — 타입 그대로 검증
const User = type({
id: 'number',
email: 'email',
age: 'number > 0',
})
TypeScript 문법 그대로 (문자열 DSL). 엄청 빠름.
Valibot — Zod의 트리쉐이킹 개선
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
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
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로. 부수 효과, 에러, 의존성을 **타입**으로 관리.
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 배우는 순서
초급
1. 기본 타입 (`string`, `number`, `boolean`, `string[]`, `Record`)
2. Interface / Type alias
3. Union / Intersection
4. Generic 기초
5. Utility types (`Partial`, `Pick`, `Omit`)
중급
6. Conditional Types
7. Mapped Types
8. `keyof`, `typeof`, `in`
9. Template Literal Types
10. `satisfies` 활용
11. Zod로 런타임 검증
고급
12. `infer` 고급 활용
13. Recursive types
14. Variance와 함수 타입
15. 타입 디버깅 (`$ExpectType`)
16. 라이브러리 수준 타입 설계
전문
17. Effect-TS
18. tRPC 풀스택
19. Type Challenges
20. TypeScript 컴파일러 API
16. 안티패턴 TOP 10
1. **`any` 남용** — 타입 시스템 무효화
2. **`// @ts-ignore` 방치** — 버그 숨기는 습관
3. **`as` 강제 캐스팅** — `satisfies`가 거의 항상 낫다
4. **Enum 사용** — `as const` union이 더 안전
5. **`Function` 타입** — 구체적 시그너처 작성
6. **과도한 타입 프로그래밍** — 컴파일 느려짐, 가독성 ↓
7. **타입과 런타임 불일치** — Zod 등으로 검증
8. **`readonly` 누락** — 불변 의도를 타입에 표현
9. **Interface와 Type 규칙 없이 혼용** — 팀 컨벤션 필요
10. **`@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를 공개했다. 당시 ...