Skip to content

필사 모드: TypeScript 타입 시스템의 깊이 — 구조적 타이핑, Generic, Conditional, satisfies, tsc Go 포팅, Zod, tRPC까지 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

> "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를 공개했다. 당시 ...

작성 글자: 0원문 글자: 11,267작성 단락: 0/342