LabHub

Blog

The Depth of TypeScript Type System — Structural Typing, Generics, Conditional, satisfies, tsc Go Port, Zod, tRPC (2025)

한국어English日本語

"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

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"


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

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

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

moduleResolution

2024 Reality


11. tsc Limits and the Go Port (2025)

tsc Problems

Alternatives

Anders' Shocking Announcement (March 2025)

"We are porting the TypeScript compiler to Go."

Why Go, Not Rust?

Significance


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)

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

Adoption


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:

Pros

Cons

2024–2025 Spread


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


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:

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)

Comments

No comments yet.

Sign in to leave a comment