Skip to content

✍️ 필사 모드: The Depth of TypeScript Type System — Structural Typing, Generics, Conditional, satisfies, tsc Go Port, Zod, tRPC (2025)

English
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 (Chief Architect of C#, creator of TypeScript)

On October 1, 2012, Microsoft announced TypeScript as "JavaScript for Application-scale Development." The reception was lukewarm — many dismissed it as "another Microsoft attempt to dominate JavaScript."

Thirteen years later, in 2025, TypeScript is the 3rd most loved language in the Stack Overflow survey. React, Vue, Angular, Next.js all adopt it as the default; 70% of new JS repos on GitHub start in TypeScript. Deno and Bun execute TypeScript natively.

This article is a map for those moving from "I write .ts files" to "I understand the depth of TypeScript."


1. Gradual Typing — The Formula of Success

Why Other Attempts Failed

  • CoffeeScript (2009) — better syntax, no types
  • Dart (2011) — Google, failed by demanding its own runtime
  • Flow (2014) — Facebook, type-only addition, lost to TS
  • Nim, ReasonML — stuck in niches

Why TypeScript survived:

  1. Gradual adoption — migrate file by file
  2. JavaScript superset — every JS is valid TS
  3. Pure JS after compile — zero runtime dependency
  4. Strong inference — minimal annotations
  5. Huge community — DefinitelyTyped with 170k+ packages

"It Succeeded by Giving Up Soundness"

  • TypeScript deliberately abandoned soundness
  • Allows any, type assertions (as), type intersections
  • Result: pragmatic
  • Flow was stricter but lost on practicality

2. Structural Typing

Nominal vs Structural

Nominal (Java, C#): type "names" must match

class Point { int x, y; }
class Vec2 { int x, y; }

Point p = new Vec2();  // Error! different class

Structural (TypeScript, Go): matching shape is enough

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, same shape

The Static Version of Duck Typing

"If it walks like a duck and quacks like a duck, it's a duck" — Python does this at runtime, TypeScript does it at compile time.

Pros

  • Flexibility — cross-library type compatibility
  • Refactoring — renaming an interface has no impact
  • Easy mocking — structure alone is enough

Cons — The Need for Branded Types

type UserId = string
type PostId = string

function getUser(id: UserId) { /* ... */ }
const postId: PostId = 'p123'
getUser(postId)  // OK by structure... bug!

Fix:

type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }

const userId = 'u1' as UserId
const postId = 'p1' as PostId
getUser(postId)  // Error!

TypeScript 5.6 (2024) documented Branded Types as an official pattern.


3. The Art of Inference

Literal Inference

let x = 'hello'      // string
const y = 'hello'    // "hello" (literal)
const z = { a: 1 }   // { a: number }  -- not readonly

const uses the value itself as the type; let widens it.

as const — Full Immutability

const config = {
  mode: 'production',
  debug: false,
} as const
// { readonly mode: "production"; readonly debug: false }

Function Inference

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 inferred

Constrained Inference

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

4. Generics — Abstracting Functions

Basics

function identity<T>(x: T): T { return x }
const n = identity(42)       // number
const s = identity('hello')  // string

Constraints

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)         // Error

Defaults

type Page<T = any> = {
  items: T[]
  total: number
}

Variance

Function arguments are contravariant, returns are covariant. TypeScript is bivariant by default, but correct under strictFunctionTypes.


5. Conditional Types — if at the Type Level

Basics

type IsString<T> = T extends string ? true : false

type A = IsString<'hello'>  // true
type B = IsString<42>       // false

infer — Type Extraction

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 }

The standard ReturnType, Parameters, Awaited all use this mechanism.

Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never

type A = ToArray<string | number>
// string[] | number[]  (distributed over the union)

Useful Combinations

type NonNullable<T> = T extends null | undefined ? never : T
type Unwrap<T> = T extends Promise<infer U> ? U : T

6. Mapped Types

Basics

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

Modifiers — +?, -?, +readonly, -readonly

type Required<T> = { [K in keyof T]-?: T[K] }
type Mutable<T> = { -readonly [K in keyof T]: T[K] }

Key Remapping (TS 4.1+)

type Getters<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}

DeepPartial

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

7. Template Literal Types (TS 4.1, 2020)

Basics

type Greeting = `Hello, ${string}`
const g: Greeting = 'Hello, World'  // OK

Combinations

type Color = 'red' | 'blue'
type Size = 'sm' | 'lg'
type Class = `${Color}-${Size}`
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"

Parsing with infer

type ExtractRoute<T> =
  T extends `/users/${infer Id}/posts/${infer PostId}`
    ? { id: Id; postId: PostId }
    : never

Router type safety (used internally by Next.js, Remix).

Famous Use — Tailwind IntelliSense

Thousands of classes like text-red-500, bg-blue-200 expressed as types. Impossible without template literal types.


8. satisfies — The Small Revolution of 2022

The Problem

type Config = { [key: string]: string | number }

const config = {
  name: 'app',
  port: 3000,
} as Config

config.name.toUpperCase()  // Error! string | number

as casts lose the concrete type.

satisfies (TS 4.9)

const config = {
  name: 'app',
  port: 3000,
} satisfies Config

config.name.toUpperCase()  // OK, inferred as string
config.port.toFixed(2)     // OK, inferred as number

"Check that this satisfies the type, but keep the concrete one."

Real Example

type Themes = Record<string, { primary: string; secondary: string }>

const themes = {
  light: { primary: '#fff', secondary: '#000' },
  dark: { primary: '#000', secondary: '#fff' },
} satisfies Themes

With as Themes, primary becomes string; with satisfies, the literal is preserved.


9. Type-Level Programming — Type Challenges

GitHub's type-challenges/type-challenges is a set of puzzles solved entirely in TypeScript types.

Implementing 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]
}

Why Valuable

  • Understand the limits and power of the type system
  • Design library-level types
  • Brain strength training

But avoid excessive type programming in production — it slows compilation and hurts readability.


10. Module Resolution Complexity

CommonJS vs ESM

// CommonJS
const mod = require('./foo')
module.exports = { /* ... */ }

// ESM
import mod from './foo'
export default { /* ... */ }

Node.js supports both, and TypeScript brokering between them is hell.

module Option

  • commonjs, esnext, node16/nodenext, preserve

moduleResolution

  • node, node16/nodenext, bundler (TS 5.0+)

2024 Reality

  • Library authors shoulder dual package (CJS + ESM)
  • Conditional resolution via exports
  • Trend toward ESM-only (Vite, Vitest, Remix)

11. tsc Limits and the Go Port (2025)

tsc Problems

  • Written in TypeScript (dogfooding)
  • Large projects: 30s–3min compile times
  • LSP response: 1–2s
  • DX bottleneck

Alternatives

  • swc — Rust, ~20x faster, no type check
  • esbuild — Go, 100x faster, no type check
  • Bun — Zig, native execution

Anders' Shocking Announcement (March 2025)

"We are porting the TypeScript compiler to Go."

  • Led by Anders Hejlsberg
  • 10x faster compile, 8x faster LSP
  • Parallelizable (escapes JS single-thread)
  • 100% compatibility
  • Target: 2026–2027 GA

Why Go, Not Rust?

  • "Equivalent performance, lower learning curve, big stdlib"
  • Has GC — easier to port existing architecture
  • "Rust would have taken 2 more years"

Significance

  • The biggest change to tsc in 10 years
  • DX revolution for giant monorepos (Slack, Airbnb, Microsoft)

12. Runtime Validation — Zod, ArkType, Valibot

Problem — Types Are Compile-Time Only

function parseUser(json: string): User {
  return JSON.parse(json)  // no runtime check!
}

External data (API, forms, files) needs runtime validation.

Zod — Since 2020, Current Standard

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)
  • One schema → type + validation
  • React Hook Form, tRPC integrations

ArkType

import { type } from 'arktype'

const User = type({
  id: 'number',
  email: 'email',
  age: 'number > 0',
})

TypeScript-syntax DSL strings. Extremely fast.

Valibot

import { object, string, email, number, minValue, pipe } from 'valibot'

const UserSchema = object({
  email: pipe(string(), email()),
  age: pipe(number(), minValue(0)),
})

Individual function imports → 70% smaller bundle.

Comparison

AspectZodArkTypeValibot
MaturityHighMediumMedium
Bundle sizeLargeMediumMinimal
PerformanceMediumTopFast
SyntaxMethod chainTS DSLFunctional
EcosystemLargestGrowingGrowing

13. tRPC — End-to-End Type Safety

Idea

client ──(shared types)── server

Without GraphQL, the frontend uses the backend's types directly.

Server

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

import { createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '../server/router'

const trpc = createTRPCProxyClient<AppRouter>({ /* ... */ })

const user = await trpc.getUser.query({ id: 1 })
// user is fully typed from the server query

Magic

  • Runtime: HTTP call
  • Types: shared at build time (type-only import)
  • Wrong id type → compile error
  • 80% of GraphQL's benefit, 0% of its complexity

Adoption

  • Dominant in Next.js full-stack projects
  • Led by Theo (t3-stack)
  • Remix, SvelteKit integration since 2024

14. Effect-TS — Functional Type Frontier

Idea

Haskell's Effect system in TypeScript. Side effects, errors, dependencies managed at the type level.

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)
  })

Type meanings:

  • Success: User
  • Error: DbError (checked-exception-like)
  • Required service: DbService (DI)

Pros

  • Exceptions visible in types
  • Automatic DI
  • Retries, timeouts, caching compose

Cons

  • Steep learning curve
  • Small ecosystem
  • Needs team buy-in

2024–2025 Spread

  • Discord, parts of Vercel adopt it
  • Effect Conference launched
  • De facto standard for functional TS

15. 2025 Learning Order

Beginner

  1. Basic types
  2. Interface / Type alias
  3. Union / Intersection
  4. Generic basics
  5. Utility types

Intermediate

  1. Conditional Types
  2. Mapped Types
  3. keyof, typeof, in
  4. Template Literal Types
  5. satisfies usage
  6. Runtime validation with Zod

Advanced

  1. Advanced infer
  2. Recursive types
  3. Variance
  4. Type debugging
  5. Library-level type design

Expert

  1. Effect-TS
  2. tRPC full-stack
  3. Type Challenges
  4. Compiler API

16. Top 10 Anti-patterns

  1. any abuse — nullifies the type system
  2. Dormant // @ts-ignore — hides bugs
  3. Forced as castssatisfies is almost always better
  4. Enumsas const unions are safer
  5. Function type — write concrete signatures
  6. Excessive type programming — slow compile, poor readability
  7. Type/runtime mismatch — validate with Zod
  8. Missing readonly — express immutability in types
  9. Interface vs Type without rules — need team conventions
  10. Mismatched @types/* versions — align with runtime

17. TypeScript Checklist

  • strict: true
  • noUncheckedIndexedAccess
  • Runtime validation (Zod/ArkType/Valibot)
  • Minimize as, prefer satisfies
  • Branded Types
  • tsconfig paths
  • moduleResolution: bundler (Vite/Next.js)
  • isolatedModules
  • CI tsc --noEmit
  • typescript-eslint
  • Type coverage measurement
  • Contribute to DefinitelyTyped

Closing — A Tool, Not a Language

Anders Hejlsberg once said:

"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."

Between perfect type safety (Haskell, Rust) and total freedom (dynamic languages), TypeScript holds the peak of pragmatism. That flexibility looks wishy-washy at first, but after 10 years of accumulation, it's the tool that most dramatically boosts large-team productivity.

TypeScript in 2025:

  • Being ported to Go for 10x speed
  • Tight integration with runtime validators like Zod
  • End-to-end type safety via tRPC
  • Functional frontier with Effect-TS

The language revolution is not over.


"TypeScript is what JavaScript could have been, if JavaScript had been designed with tooling in mind." — Daniel Rosenwasser (TypeScript PM)

현재 단락 (1/291)

On October 1, 2012, Microsoft announced TypeScript as "JavaScript for Application-scale Development....

작성 글자: 0원문 글자: 11,869작성 단락: 0/291