Programming Fundamentals

Type Systems: TypeScript, Gradual Typing and Type Inference

2019. Airbnb migrates to TypeScript. A year later they publish a report. 38 percent of all production bugs could have been caught by static typing. Not 5, not 10, but 38. That is the cost of one engineering decision about a type system.

  • TypeScript ships in 76 percent of large JS projects on GitHub (State of JS 2023).
  • Python mypy at Instagram caught roughly half of critical errors before deploy.
  • Pydantic v2 (Rust core plus Python type hints) gives FastAPI request validation with zero hand-written checks.
  • Zod plus TypeScript: runtime-safe parsing of API responses without `any`.

Предварительные знания

  • Comfortable JavaScript at the ES2015 level: classes, arrow functions, destructuring, spread.
  • Understanding of the difference between static and dynamic languages (Python, JS vs Java, C#).
  • Basic Node.js or browser JS: able to install an npm package and run tsc.
  • Prior exposure to interfaces or classes in any OOP language helps but is not required.
  • Async/await and event loop
  • Testing and TDD

From Hindley-Milner (1969) to TypeScript (2012): how types reached the mainstream

Type inference is older than most languages that use it. In 1969 Roger Hindley described a principal-type algorithm for combinatory logic. In 1978 Robin Milner independently described the same idea for ML. The result, known as Hindley-Milner inference, became the backbone of Haskell, OCaml, and F#. Structural typing came out of type theory and turned practical with Standard ML and later Go (Rob Pike, Ken Thompson, Robert Griesemer in 2009). For decades the dynamic-language camp treated static types as unnecessary ceremony. Things shifted between 2009 and 2012. Brendan Eich tried optional types in JS as part of ES4 in 2009 (it was scrapped). In October 2012 Anders Hejlsberg, the same engineer behind Turbo Pascal (1983), Delphi, and C#, presented TypeScript 0.8 at Microsoft. The goal was gradual typing on top of existing JavaScript with no breaking changes. In 2014 Facebook released Flow with a similar pitch, but TypeScript won the market thanks to deep VS Code integration (also Microsoft) and rapid feature delivery: generics in 1.0 (2014), conditional types in 2.8 (2018), template literal types in 4.1 (2020). Python took the same path. PEP 484 by Guido van Rossum and Jukka Lehtosalo arrived in 2014, mypy became the standard checker, and pyright from Microsoft brought a fast alternative. By 2024 static types were the default for large codebases even in historically dynamic languages.

Static, Dynamic, Structural, Nominal

TypeScript compiles a billion lines of code inside Google every day. Python - the language powering GPT-4, NumPy, the entire ML stack - is dynamically typed. Both are right. The question is not 'which is better' but 'when is each needed'. A static type system catches errors before execution. A dynamic one allows rapid, flexible development. Gradual typing sits between.

**Nominal vs structural typing** is the key divide between languages. In **nominal** typing (Java, C#), a type is defined by its name: two classes with identical fields are different types. In **structural** typing (TypeScript, Go), a type is defined by its shape: if an object has the required fields, it is compatible. TypeScript is structural: a type `Duck` is compatible with interface `Animal` if it has `speak()`, without any explicit `implements`.

**Gradual typing**: Python 3.5+ introduced type hints via PEP 484. They are optional and have no runtime effect. mypy and pyright are static analysers that check them during development. FastAPI uses Pydantic + type hints to auto-validate request bodies and generate OpenAPI schemas with no additional configuration code.

TypeScript uses structural typing. What does this mean for type compatibility?

Generics, Utility Types and Conditional Types

Generic types are functions over types. Just as `Math.max` takes numbers and returns a number, `Array<T>` takes a type and returns a type. Without generics one would need `NumberArray`, `StringArray`, `UserArray` - and lose element type information every time. TypeScript generics eliminate `any` where parametric polymorphism is needed.

**Mapped types** transform every field of a type via the `[K in keyof T]` syntax. Utility types (`Partial`, `Readonly`, `Required`) are implemented through mapped types in the TypeScript standard library. Understanding mapped types enables writing custom type transformations for any code pattern.

How does `Pick<User, 'id' | 'name'>` differ from `Partial<User>`?

Type Narrowing, Discriminated Unions and infer

**Type narrowing** is the process by which TypeScript refines the type of a variable based on context. After `if (typeof x === 'string')` TypeScript knows that inside the block `x` is a string. This is not a trick - it is data flow analysis: the compiler builds a graph of all possible values and narrows it at each branch. The same principle is used in compilers for optimisation.

**Exhaustiveness checking** is the final discriminated union pattern. Adding `never` to a default switch branch causes TypeScript to raise an error when a new union variant is added without handling. This makes the type system a living document: add a new event type and the compiler points to every location that needs updating.

TypeScript is just JavaScript with annotations that are erased at compile time and have no effect on program behaviour.

The TypeScript type system influences behaviour through tooling: autocomplete, refactoring, catching errors before running. Strong typing changes architectural decisions and reduces production bugs.

TypeScript does erase types to JS. But the impact of the type system is in the development process: one cannot accidentally pass null where a string is expected, the compiler enforces handling all union cases. This changes how code is written, not how it executes.

Why use a discriminated union instead of a plain union type?

Related Topics

The type system runs through all development: from architecture to tests.

  • Async/Await and Event Loop — Promise<T> is a generic type; await unwraps T from Promise<T>
  • Testing and TDD — Strong types reduce the need for edge-case testing

Key Ideas

  • TypeScript structural typing: compatibility by shape, not by name - compile-time duck typing
  • Generic types parameterise logic: Array<T>, Promise<T>, Result<T, E> - one implementation for all types
  • Utility types (Partial, Pick, Omit, Record) transform types without code duplication
  • Discriminated unions + narrowing = exhaustive type safety without runtime checks
  • infer extracts types from conditional expressions: ReturnType<F>, Unpacked<T>

Вопросы для размышления

  • When can structural typing create unexpected type compatibility, and how can branded types prevent it?
  • Conditional types can cause exponential growth in compilation time. How does one diagnose and fix this?
  • How does gradual typing with Python mypy help when migrating a legacy codebase without a full rewrite?

Связанные уроки

  • prog-16-async — Coroutines use generic types like Promise<T>
  • prog-15-testing — Strong typing reduces the number of tests needed for edge cases
  • prob-04-bayes — Type inference derives types from context, like Bayesian inference from data
  • alg-01-big-o — Type system complexity applies at compile time, not runtime
Type Systems: TypeScript, Gradual Typing and Type Inference

0

1

Sign In