Skip to content

TypeScript 速查表

JavaScript 的类型化超集,可扩展。

01

入门

Hello World 与编译

TypeScript 文件使用 .ts 扩展名。tsc 编译器将 TS 转译为 JS,在运行时擦除所有类型注解。使用 --strict 获得最大类型安全。ts-node 或 bun 可直接运行 .ts 文件而无需单独编译步骤。

typescript
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);

// Compile to JavaScript (type annotations erased):
//   tsc hello.ts          -> hello.js
//   tsc --strict hello.ts  // enable all strict checks
//   tsc --watch hello.ts   // recompile on change

// Run directly with ts-node or bun:
//   ts-node hello.ts

tsconfig.json

tsconfig.json 配置 TypeScript 编译器。'strict: true' 启用 noImplicitAny、strictNullChecks、strictFunctionTypes 等。'target' 控制输出 JS 版本。'esModuleInterop' 启用从 CommonJS 模块(如 Node 内置模块)的默认导入。

typescript
{
  "compilerOptions": {
    "target": "ES2020",        // JS version to emit
    "module": "ESNext",        // module system
    "strict": true,            // enable all strict checks
    "outDir": "./dist",        // output directory
    "rootDir": "./src",        // source root
    "esModuleInterop": true,   // allow default imports from CJS
    "skipLibCheck": true,      // skip .d.ts type checking
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

类型注解与推断

类型注解显式指定变量的类型。TypeScript 也可从值推断类型。函数签名和公共 API 使用显式注解;局部变量依赖推断。避免 'any' —— 它完全退出类型检查。

typescript
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];

// Type inference (let TS figure it out)
let age = 30;           // inferred as number
let items = [1, 2, 3];  // inferred as number[]
let mixed = [1, "a"];   // inferred as (string | number)[]

// any disables type checking (avoid!)
let anything: any = 42;
anything = "string";    // no error

严格模式检查

严格模式启用关键检查:strictNullChecks(null/undefined 不可赋值给其他类型)、noImplicitAny(参数必须有类型)、strictPropertyInitialization(类字段必须初始化)。当确定字段稍后会被设置时使用 '!'(确定赋值)。

typescript
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode

// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any

// strictPropertyInitialization
class User {
  name: string; // Error: not initialized
  constructor() {}
}

// Fix: initialize or use definite assignment
class Fixed {
  name!: string; // definite assignment assertion
  age: number = 0;
}

声明文件 (.d.ts)

声明文件 (.d.ts) 为没有 TypeScript 定义的 JavaScript 库提供类型。'declare' 告诉编译器变量/函数在运行时存在。使用 DefinitelyTyped 的 @types 包获取流行库(如 @types/node、@types/react)。

typescript
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
  export function greet(name: string): string;
  export const version: string;
}

// global.d.ts - extend global scope
declare global {
  interface Window {
    myApp: { version: string };
  }
}

// Usage in .ts files:
// import { greet } from "my-lib";  // now typed
// Install @types packages: npm i -D @types/node @types/react
02

基本类型

基本类型与特殊类型

TypeScript 基本类型:string、number、boolean、bigint、symbol。'void' 表示函数不返回值。'never' 表示永不出现的值 —— 抛出或永远运行的函数。在 switch 语句中使用 'never' 进行穷尽检查。

typescript
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");

// void: function returns nothing
function log(msg: string): void { console.log(msg); }

// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }

数组与元组

数组使用 T[] 或 Array<T> 语法。ReadonlyArray 防止修改。元组是固定长度数组,每个位置有特定类型 —— 适用于键值对或类 CSV 数据。标记元组通过命名位置提高可读性。

typescript
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable

// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1];  // number

// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];

枚举

枚举定义一组命名常量。推荐使用字符串枚举(输出中值可读)。数字枚举支持反向映射。'const enum' 在编译时被擦除(内联)以实现零运行时成本。简单情况优先使用联合类型。

typescript
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up;  // 0

// String enum (recommended for readability)
enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"

// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

any vs unknown vs never

'any' 禁用所有类型检查 —— 避免使用。'unknown' 是类型安全的替代:使用前必须收窄(通过 typeof、instanceof)。'never' 表示永不出现的值,用于 switch 语句中的穷尽检查以在编译时捕获遗漏情况。

typescript
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string";       // OK
a.toUpperCase();    // OK (no check, may fail at runtime)

// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
  u.toUpperCase();  // OK after narrowing
}

// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return Math.PI;
    case "square": return 1;
    default:
      const _exhaustive: never = s; // Error if case missing
  }
}

类型断言

类型断言告诉编译器'相信我,我知道类型'。使用 'as' 语法。非空断言 (!) 告诉 TS 值不是 null/undefined。'as const' 使所有属性成为只读字面量 —— 适用于配置对象和 Redux action 类型。断言不改变运行时行为。

typescript
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;

// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi";  // el is HTMLElement, not null

// Double assertion (for unsafe casts)
let value = "42" as unknown as number;

// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly

字面量与联合类型

字面量类型将值限制为特定字符串、数字或布尔值。与联合结合,创建精确类型如方向或 HTTP 方法。模板字面量类型 (TS 4.1+) 从其他类型构建字符串类型 —— 强大用于生成类型安全的键和事件名。

typescript
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left";  // OK
// direction = "sideways"; // Error

// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4;  // OK

// Boolean literal
let flag: true = true;

// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
03

接口与对象

接口基础

接口描述对象的形状。'?' 标记可选属性(可能为 undefined)。'readonly' 防止初始化后重新赋值。接口仅在编译时存在 —— 在输出 JavaScript 中被擦除。用于为对象和类定义契约。

typescript
interface User {
  id: number;
  name: string;
  email?: string;        // optional property
  readonly createdAt: Date; // immutable
}

const user: User = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined

索引签名

索引签名允许具有给定类型的任意键的对象。所有属性值必须可赋值给索引类型。适用于字典、缓存和动态数据。与已知属性结合用于带额外选项的类型化配置。

typescript
// Index signature: arbitrary string keys
interface StringMap {
  [key: string]: string;
}

const dict: StringMap = {
  name: "Alice",
  city: "NYC",
  // count: 42, // Error: value must be string
};

// Mixed: known + index signature
interface Config {
  name: string;
  [key: string]: string | number;
}

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}

扩展接口

接口可扩展一个或多个其他接口,组合其成员。这实现了组合和代码复用。与类不同,接口支持多重继承。实现接口时,类必须提供所有必需成员。

typescript
interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

interface Swimmer {
  swim(): void;
}

// Multiple inheritance
interface Duck extends Pet, Swimmer {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  owner: "Walt",
  eat() {},
  play() {},
  swim() {},
  quack() {},
};

接口中的函数类型

接口可描述函数签名,实现类型安全回调。混合接口(可调用 + 属性)用于 jQuery 风格的函数,这些函数也有方法。此模式在返回带附加辅助函数的函数的库中常见。

typescript
interface SearchFn {
  (source: string, keyword: string): boolean;
}

const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true

// Interface with mixed members (hybrid)
interface Counter {
  (start: number): void;  // callable
  count: number;           // property
  reset(): void;           // method
}

// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };

接口 vs 类型别名

接口支持声明合并(同名接口合并)、更好的错误消息,优先用于对象/类形状。类型别名更灵活(可表示联合、基本类型、元组)但不能合并。可扩展 API 使用接口,联合和计算类型使用类型别名。

typescript
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };

// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;

// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };

// Use interface for objects/classes, type for unions/aliases

可选链与空值合并

可选链 (?.) 安全访问嵌套属性 —— 如果任何链接为 null/undefined 则返回 undefined 而非抛出。空值合并 (??) 仅对 null/undefined(非 0 或 '')提供默认值。这些运算符大幅减少冗长的空检查代码。

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

const user: User = {};

// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined

// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";

// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky

// Optional method call
const result = user.profile?.address?.city?.toUpperCase();
04

类型别名与联合

类型别名

类型别名为任何类型创建命名引用,包括联合、交叉、基本类型和泛型。与接口不同,别名不能合并或扩展,但更灵活。联合、元组和工具类型使用别名;对象形状使用接口。

typescript
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };

// Generic alias
type Container<T> = { value: T };

// Function type alias
type Handler<T> = (event: T) => void;

// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);

联合类型

联合类型 (A | B) 允许值是多种类型之一。TypeScript 在条件块内使用 typeof、instanceof 或 in 检查收窄类型。注意:(string | number)[] 与 string[] | number[] 不同 —— 前者是混合数组,后者是全字符串或全数字。

typescript
// Union: value can be one of several types
type ID = string | number;

function display(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // narrowed to string
  } else {
    console.log(id.toFixed(2));    // narrowed to number
  }
}

display("abc");  // ABC
display(42);     // 42.00

// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];

交叉类型

交叉类型 (A & B) 组合多种类型的所有成员 —— 结果必须满足每个类型。适用于 mixin、组合和合并工具类型。与联合 (OR) 不同,交叉是 AND:值必须具有所有类型的所有属性。

typescript
// Intersection: combine multiple types into one
interface BusinessPartner {
  name: string;
  credit: number;
}

interface Identity {
  id: number;
  email: string;
}

type Employee = BusinessPartner & Identity;

const emp: Employee = {
  name: "Alice",
  credit: 1000,
  id: 1,
  email: "[email protected]",
};

// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props

可空类型

严格模式下,null 和 undefined 不可赋值给其他类型 —— 必须用联合显式包含 (string | null)。可选参数 (param?) 隐式为 T | undefined。使用 ?? 进行安全默认值,用 ! 断言非空(谨慎使用)。

typescript
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice";  // OK

// Optional parameters are implicitly | undefined
function greet(name?: string) {
  // name is string | undefined
  console.log(name ?? "Guest");
}

// Return type can be null
function find(id: number): string | null {
  return id === 1 ? "Alice" : null;
}

// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"

Keyof 与 Typeof 运算符

'keyof T' 将类型 T 的键提取为字符串字面量联合。'typeof x' 提取值的类型(适用于从对象推断)。'keyof typeof obj' 结合两者获取现有对象的键 —— 在 Redux action 类型和类型安全属性访问器中常见。

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"

function getProp(obj: User, key: keyof User) {
  return obj[key];
}

// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }

// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"

映射类型

映射类型遍历键以转换类型。内置工具如 Readonly、Partial 和 Pick 是映射类型。使用 + 和 - 修饰符添加/移除 readonly 或可选。键重映射 (TS 4.1+) 使用模板字面量类型重命名键 —— 强大用于生成 getter/setter 类型。

typescript
// Map over keys to create a new type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface User { id: number; name: string; }

type ReadonlyUser = Readonly<User>;   // all readonly
type OptionalUser = Optional<User>;   // all optional

// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };

// Key remapping (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
05

函数

函数类型与签名

TypeScript 为函数参数和返回值添加类型注解。返回类型通常可推断,但公共 API 推荐显式注解。默认参数使参数可选并带后备值。函数不返回任何内容时使用 void。

typescript
// Named function with types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;

// Void return (no return value)
function log(msg: string): void { console.log(msg); }

// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
  return `${greeting}, ${name}!`;
}
greet("Alice");        // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"

剩余参数与元组

剩余参数 (...args) 将多个参数收集到数组中。TypeScript 将它们类型化为 T[] 或元组用于固定长度可变参数函数。展开运算符 (...) 执行相反操作 —— 将数组展开为单独参数。元组剩余类型实现精确的可变参数签名。

typescript
// Rest parameters (variadic)
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
  console.log(name, scores);
}

// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));

// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
  const [s, n, b] = args;
}

函数重载

函数重载为同一函数提供多个类型签名,基于输入实现精确返回类型。实现签名对调用者隐藏。重载自上而下解析 —— 将更具体的签名放在前面。在 jQuery 和 lodash 等库中常见。

typescript
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return [input, input * 2];
}

const strs = parse("a,b,c"); // string[]
const nums = parse(42);      // number[]

// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
  return m === undefined
    ? new Date(yOrTs)
    : new Date(yOrTs, m - 1, d);
}

this 类型

TypeScript 允许将 'this' 类型声明为第一个参数。这确保函数以正确上下文调用 —— 适用于作为回调传递的方法。箭头函数词法捕获 'this',避免 .bind() 的需要。使用 'noImplicitThis' 捕获未类型化的 'this' 错误。

typescript
interface Card {
  suit: string;
  rank: string;
  isFaceUp(): boolean;
}

// Explicit 'this' parameter
function format(this: Card): string {
  return `${this.rank} of ${this.suit}`;
}

const card: Card = {
  suit: "Hearts",
  rank: "A",
  isFaceUp() { return true; },
  format,
};

// 'this' in callbacks with bind
class Handler {
  private count = 0;
  increment = () => { this.count++; }; // arrow binds this
}

回调与高阶函数

TypeScript 完全类型化高阶函数(接受或返回函数的函数)。使用泛型类型参数 (T, U) 保留输入和输出之间的类型关系。回调类型通常定义为类型别名以便复用。柯里化(返回函数)完全类型安全。

typescript
// Function as parameter
type Callback<T> = (value: T, index: number) => void;

function forEach<T>(arr: T[], cb: Callback<T>): void {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
  }
}

forEach(["a", "b"], (v, i) => console.log(i, v));

// Function returning function (curry)
function add(a: number): (b: number) => number {
  return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8

// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  return arr.map(fn);
}

参数解构

TypeScript 支持函数参数中的解构 —— 内联注解解构形状或通过接口。提取到接口提高可读性和复用。数组/元组解构也可用。此模式在 React 组件 props 和 API 处理程序中常见。

typescript
// Destructured parameters with types
function createUser({ name, age, email }: {
  name: string;
  age: number;
  email?: string;
}): void {
  console.log(name, age, email);
}

createUser({ name: "Alice", age: 30 });

// Extract to interface for reuse
interface UserOpts {
  name: string;
  age: number;
  email?: string;
}

function updateUser({ name, age }: UserOpts): void {}

// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
  return [b, a];
}
06

类与 OOP

类与构造函数

TypeScript 类支持参数属性 —— 用访问修饰符 (public/private/protected/readonly) 前缀构造函数参数会自动创建并赋值字段。此简写减少样板代码。方法可在返回值上有类型注解。字段默认为 public。

typescript
class Person {
  // Parameter properties (shorthand)
  constructor(
    public name: string,    // auto-creates this.name
    private age: number,    // private field
    readonly id: number,    // immutable
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const p = new Person("Alice", 30, 1);
console.log(p.name);  // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly

访问修饰符

访问修饰符:public(默认,到处)、private(仅类)、protected(类 + 子类)、readonly(不可变)。TypeScript 的 'private' 仅编译时;ES '#' 私有字段在运行时真正私有。private 用于实现细节,protected 用于扩展点。

typescript
class BankAccount {
  public owner: string;       // accessible everywhere
  private balance: number;    // class only
  protected rate: number;     // class + subclasses
  readonly id: string;        // immutable after init
  #secret: string;            // ES private (runtime)

  constructor(owner: string) {
    this.owner = owner;
    this.balance = 0;
    this.rate = 0.05;
    this.id = crypto.randomUUID();
    this.#secret = "hidden";
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

继承与抽象类

抽象类不能直接实例化 —— 它们为子类定义基类。抽象方法在基类中没有实现;子类必须实现。使用 'extends' 继承,'super()' 调用父构造函数。抽象类实现多态 —— 代码可适用于任何 Shape 子类。

typescript
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;  // must be implemented
  describe(): string {
    return `${this.color} shape, area ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private r: number) {
    super(color);
  }
  area(): number { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(color: string, private side: number) {
    super(color);
  }
  area(): number { return this.side ** 2; }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract

接口与实现

一个类可实现多个接口(用逗号分隔)。类必须提供所有接口成员。与 extends(单继承)不同,implements 支持多个契约。这是 TypeScript 实现类似多重继承行为的方式。用接口定义契约,用类实现它们。

typescript
interface Printable {
  toString(): string;
}

interface Comparable<T> {
  compareTo(other: T): number;
}

class Money implements Printable, Comparable<Money> {
  constructor(private amount: number) {}

  toString(): string {
    return `$${this.amount.toFixed(2)}`;
  }

  compareTo(other: Money): number {
    return this.amount - other.amount;
  }
}

const a = new Money(10);
const b = new Money(20);
console.log(a.toString());      // "$10.00"
console.log(a.compareTo(b));    // -10

Getter 与 Setter

Getter 和 setter 拦截属性访问用于验证、计算或副作用。使用私有后备字段(约定:下划线前缀)。Getter 实现计算属性(如从摄氏度得到华氏度)。Setter 实现验证。像常规属性一样访问 —— 无括号。

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) throw new Error("Below absolute zero");
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws

静态成员与单例

静态成员属于类而非实例 —— 通过 ClassName.member 访问。static 用于常量、工具函数和工厂方法。私有构造函数 + 静态 getInstance() 实现单例模式。'as const' 使静态数组成为带字面量类型的只读。

typescript
class Logger {
  static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent direct new

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;

  log(msg: string): void {
    this.logs.push(msg);
  }
}

const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor
07

泛型

泛型函数

泛型 (<T>) 让你编写适用于任何类型同时保留类型安全的函数。类型参数 T 是在调用时填充的占位符 —— 显式 (identity<number>) 或从参数推断。泛型实现可复用、类型安全的数据结构和算法。

typescript
// Generic function: preserves type relationship
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(42);    // T = number, returns number
const s = identity("hi");          // T inferred as string

// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("Alice", 30); // [string, number]

// Generic with array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

泛型类

泛型类 (<T>) 创建类型安全容器。每个实例锁定特定类型 —— Stack<number> 只接受数字。这在编译时捕获类型错误且无运行时开销(泛型被擦除)。常见于集合 (Stack, Queue, Map) 和响应式包装器 (Observable<T>)。

typescript
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2

const strStack = new Stack<string>();
strStack.push("hello");

泛型约束

约束 (T extends SomeType) 限制泛型可接受的类型。'T extends HasLength' 确保 T 有 'length' 属性。'K extends keyof T' (keyof 约束) 确保键存在于对象上,返回正确的值类型。约束实现泛型上的类型安全属性访问和方法调用。

typescript
// Constraint: T must have a 'length' property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");     // 5 (string has length)
logLength([1, 2, 3]);   // 3 (array has length)
// logLength(42);        // Error: number has no length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a key

默认类型参数

默认类型参数在未指定时提供后备类型。适用于有常见情况的 API(如 ApiResponse 默认为 string)。默认值可依赖前面的参数。与约束结合 (T extends X = DefaultType) 实现类型安全的可选泛型。

typescript
// Default type parameter
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

const r1: ApiResponse = { data: "hello", status: 200 };       // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 };    // T = number

// Multiple defaults
class Container<T = string, U = number> {
  constructor(public a: T, public b: U) {}
}

const c1 = new Container("hi", 42);      // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>

// Default with constraint
interface Box<T extends object = { id: number }> {
  value: T;
}

泛型接口与类型

泛型接口和类型别名创建可复用、类型安全的契约。Repository<T> 用一致 API 抽象数据访问。Result<T, E> 是用于无异常错误处理的区分联合。默认类型参数 (E = Error) 减少常见情况的样板代码。

typescript
// Generic interface
interface Repository<T> {
  findById(id: string): Promise<T>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implement with concrete type
class UserRepo implements Repository<User> {
  async findById(id: string): Promise<User> { /* ... */ }
  async save(user: User): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// Generic type alias with conditional
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };

条件类型

条件类型 (T extends U ? X : Y) 是类型级 if 语句。'infer R' 从另一类型内提取类型(如函数返回类型)。条件类型在联合上分发 —— ToArray<string | number> 变为 string[] | number[]。Exclude、Extract 和 NonNullable 等内置工具使用此特性。

typescript
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

// Extract return type
type AsyncReturnType<T> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;
08

高级类型

工具类型

TypeScript 提供常用转换的内置工具类型:Partial(全可选)、Pick(选择键)、Omit(排除键)、Record(键值映射)、Required(移除可选)、ReturnType(函数返回)、Parameters(函数参数为元组)。这些消除重复的类型定义。

typescript
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };

// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;

// Record: key-value map
type UserMap = Record<string, User>;

// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;

// ReturnType: function return type
type R = ReturnType<() => string>; // string

// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]

模板字面量类型

模板字面量类型 (TS 4.1+) 通过插值其他类型构建字符串类型。与联合结合,生成字符串的笛卡尔积。Capitalize/Uppercase 转换大小写。用于类型安全的事件名、getter/setter 生成和 API 路由类型化。映射类型中的 'as' 子句启用键重命名。

typescript
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">;    // "Foo"

// Getter names from keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"

infer 关键字

'infer' 关键字在条件类型的 'extends' 子句内声明类型变量,捕获类型以复用。它是 ReturnType、Parameters 和 Awaited 等工具类型的基础。使用 infer 从复杂结构(数组、Promise、函数)中提取类型而无需手动分解。

typescript
// Extract return type of a function
type MyReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;    // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>;  // string

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number

// Extract first parameter
type FirstParam<T> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string

区分联合

区分联合(标记联合)使用共享字面量字段 ('type'、'kind'、'tag') 区分变体。TypeScript 在每个 switch case 中收窄类型,访问 case 特定字段。'never' 默认值启用穷尽检查 —— 添加新 case 时,编译器报错直到处理它。对 Redux reducer 和状态机至关重要。

typescript
// Discriminated union: shared 'type' (or 'kind') field
type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "DELETE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text }];
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      const _: never = action; // exhaustiveness check
      return state;
  }
}

类型守卫:typeof 与 instanceof

类型守卫在运行时收窄类型。'typeof' 适用于基本类型 (string, number, boolean, symbol, bigint, undefined, function, object)。'instanceof' 检查类/构造函数原型。Array.isArray() 收窄为类型化数组。这些是内置守卫 —— 无需自定义代码。TypeScript 在每个分支中跟踪收窄的类型。

typescript
// typeof: narrow primitives
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2);      // number
}

// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // Dog
  } else {
    pet.meow(); // Cat
  }
}

// Array.isArray
function flatten(arr: (number | number[])[]) {
  return arr.flatMap(x =>
    Array.isArray(x) ? x : [x]
  );
}

自定义类型守卫与 in 运算符

'in' 运算符检查属性是否存在于对象上,收窄为拥有它的类型。类型谓词 (x is T) 是返回布尔值但也收窄类型的自定义守卫函数。使用 'unknown' 作为输入类型安全解析外部数据 (JSON.parse, API 响应)。谓词实现可复用、可组合的类型检查。

typescript
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly(); // Bird
  }
}

// Type predicate: custom guard function
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: any): x is { name: string; age: number } {
  return x && typeof x.name === "string" && typeof x.age === "number";
}

const val: unknown = "hello";
if (isString(val)) {
  console.log(val.toUpperCase()); // narrowed to string
}
09

数据结构

数组与 ReadonlyArray

TypeScript 数组是类型化的 —— map、filter 和 reduce 等方法保留元素类型。使用 readonly T[] 或 ReadonlyArray<T> 实现不可变性。元组有固定长度和类型化位置。数组解构和展开完全类型安全。类型系统捕获索引越界和错误类型赋值。

typescript
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly

// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];

// Array methods are typed
const doubled = nums.map(n => n * 2);    // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number

// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];

对象与 Record

Record<K, V> 创建键类型为 K、值类型为 V 的类型化映射。Partial<T> 使所有属性可选 —— 非常适合更新/修补操作。Object.entries/keys/values 返回类型化数组。使用 Pick 和 Omit 从现有类型派生专注类型,保持类型 DRY。

typescript
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };

// Record: typed key-value map
const scores: Record<string, number> = {
  math: 90,
  science: 85,
};

// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };

// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;

// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores);       // string[]
const values = Object.values(scores);   // number[]

Map 与 Set

Map 和 Set 是 ES6 集合,有完整 TypeScript 支持。Map 键可以是任何类型(不同于对象,对象将键强制转换为字符串)。Set 存储唯一值。WeakMap/WeakSet 允许键的垃圾回收 —— 适用于附加到 DOM 元素或对象的元数据而不阻止 GC。

typescript
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined

// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true

// Iteration (typed)
for (const [key, value] of map) {
  console.log(key, value.name);
}

for (const num of unique) {
  console.log(num);
}

// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();

元组与标记元组

元组是固定长度、类型化位置的数组。标记元组 (TS 4.0+) 添加名称以提高可读性 —— 适用于返回值和类 CSV 数据。元组无需创建接口即可实现多返回值。使用 'readonly' 防止修改。元组不同于数组:[string, number] 不是 (string | number)[]。

typescript
// Basic tuple
let point: [number, number] = [10, 20];

// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];

// Destructuring with labels
const [id, name, active] = user;

// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
  return [Math.floor(a / b), a % b];
}

const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2

// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // Error

枚举与 Const 枚举

枚举创建命名常量。推荐字符串枚举(输出可读,无反向映射问题)。Const 枚举在编译时擦除(零运行时成本)。简单情况下,联合类型 ('a' | 'b') 通常更好 —— 无运行时代码,更好的 tree-shaking。分组、文档化的常量使用枚举。

typescript
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }

// String enum (recommended)
enum HttpStatus {
  OK = "200 OK",
  NotFound = "404 Not Found",
  ServerError = "500 Internal Server Error",
}

// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";

// Exhaustive switch
function handle(s: Status) {
  switch (s) {
    case "pending": return "loading";
    case "success": return "done";
    case "error": return "failed";
  }
}

不可变数据

TypeScript 提供多种不可变性工具:属性的 'readonly'、数组的 ReadonlyArray、Readonly<T> 工具,以及用于深度只读字面量类型的 'as const'。不可变数据防止意外修改并启用变更检测 (React, Redux)。使用展开 (...) 进行不可变更新 —— 创建带修改字段的新对象。

typescript
// Readonly modifier
interface User {
  readonly id: number;
  name: string;
}

// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error

// Readonly utility type
type FrozenUser = Readonly<User>;

// as const: deep readonly with literal types
const config = {
  endpoint: "/api",
  methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]

// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new object
10

模块与命名空间

ES 模块:导入与导出

TypeScript 使用 ES 模块语法 (import/export)。命名导出是显式的;默认导出是单个'主要'导出。使用 'import * as' 进行命名空间导入。模块解析遵循 Node.js 约定 (node_modules, 扩展名)。在 tsconfig.json 中配置 'module' 和 'moduleResolution'。

typescript
// math.ts - exporting
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a: number, b: number): number {
  return a * b;
}

// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import

console.log(add(1, 2));      // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI);        // 3.14159

仅类型导入

'import type' 仅导入类型(编译时擦除,无运行时代码)。这避免循环依赖和不必要的运行时导入。TS 4.5+ 允许在混合导入中内联 'type' 修饰符。接口、类型别名和枚举(如果是 const)使用仅类型导入以减小包大小。

typescript
// Type-only import (erased at runtime)
import type { User, Config } from "./types";

// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";

// Re-export types
export type { User } from "./types";

// Import type for interfaces and types
interface UserService {
  get(id: string): User;  // User from type-only import
}

// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoid

动态导入

动态导入 (import()) 按需加载模块,返回 Promise。这实现代码分割和懒加载 —— 对 Web 应用性能至关重要。TypeScript 自动推断模块类型。用于可选功能、大型库和基于路由的代码分割 (React.lazy, Next.js dynamic)。

typescript
// Dynamic import returns a Promise
async function loadModule() {
  const module = await import("./heavy-module");
  module.doWork();
}

// Conditional loading
if (featureEnabled) {
  const { Feature } = await import("./feature");
  new Feature().init();
}

// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");

// With error handling
try {
  const lib = await import("./optional-lib");
  lib.run();
} catch (e) {
  console.log("Library not available");
}

声明文件与模块增强

声明文件 (.d.ts) 描述 JS 模块、CSS/PNG 导入和全局变量的类型。模块增强扩展现有模块类型 —— 适用于向 Express Request、Express Response 或第三方类型添加属性。这是 passport 等中间件添加 req.user 类型化的方式。

typescript
// global.d.ts - declare ambient modules
declare module "*.css" {
  const content: string;
  export default content;
}

declare module "*.png" {
  const src: string;
  export default src;
}

// Module augmentation - extend existing types
import express from "express";

declare module "express" {
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now req.user is typed
app.get("/", (req, res) => {
  console.log(req.user?.name);
});

命名空间(遗留)

命名空间是 TypeScript 的 ES6 之前模块系统。它们将相关代码分组在命名对象下。对于新项目,优先使用 ES 模块 (import/export) —— 它们标准化、可 tree-shake,且与打包器配合工作。命名空间在 .d.ts 声明文件中用于全局类型声明和遗留代码时仍有用。

typescript
// Namespace: pre-ES6 way to organize code
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true

// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarations

tsconfig 模块设置

tsconfig 模块设置控制 TypeScript 如何处理导入。'moduleResolution: node' 使用 Node.js 解析 (node_modules 查找)。'esModuleInterop' 启用从 CommonJS 的默认导入。'paths' 创建导入别名 (@/components) 以获得更清晰的导入。'resolveJsonModule' 允许导入带推断类型的 .json 文件。

typescript
{
  "compilerOptions": {
    "module": "ESNext",           // ES module output
    "moduleResolution": "node",   // Node-style resolution
    "esModuleInterop": true,      // allow default imports from CJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // import .json files
    "isolatedModules": true,      // each file is independent
    "baseUrl": "./src",           // base for non-relative imports
    "paths": {
      "@/*": ["./*"],             // path alias
      "@components/*": ["./components/*"]
    }
  }
}

// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"
11

异步与 Promise

Promise 类型

Promise<T> 是核心异步类型 —— T 是解析值类型。TypeScript 通过 .then() 链推断类型。使用 'new Promise()' 包装基于回调的 API。始终类型化 resolve/reject 值。优先使用 async/await 而非原始 .then() 链以获得可读性和错误处理。

typescript
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
  // user is User, not any
  console.log(user.name); // string
});

async/await

async/await 是 Promise 的语法糖 —— 'await' 暂停直到 Promise 解析。async 函数始终返回 Promise。使用 Promise.all() 进行并行执行(比顺序 await 快得多)。顶层 await 在 ES 模块和 ES2022+ 中工作。TypeScript 检查 await 的值是否为 Promise。

typescript
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);

// Sequential vs parallel
async function loadAll() {
  // Sequential (slow)
  const a = await getUser(1);
  const b = await getUser(2);

  // Parallel (fast)
  const [x, y] = await Promise.all([getUser(1), getUser(2)]);
  return { a, b, x, y };
}

异步中的错误处理

使用 try/catch 配合 async/await 进行错误处理 —— 比 .catch() 更清晰。TypeScript 不支持类型化 throw(catch 中所有错误为 unknown),因此用 instanceof 收窄。对于可预测错误,考虑 Result 类型模式 (ok/error 联合) 而非异常 —— 它使错误处理在类型签名中显式。

typescript
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
  try {
    const data = await fetch("/api/data");
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.text();
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}

// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

// Result type as alternative to exceptions
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function safeFetch(url: string): Promise<Result<string>> {
  try {
    const res = await fetch(url);
    return { ok: true, value: await res.text() };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

Promise 组合器

Promise 组合器编排多个异步操作:all()(并行,快速失败)、allSettled()(并行,等待全部)、race()(第一个完成)、any()(第一个成功)。依赖数据加载用 all(),想要部分结果用 allSettled(),超时用 race(),冗余获取用 any()。

typescript
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
  fetch("/api/a"),
  fetch("/api/b"),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow"),
]);

// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
  fetch("/api/primary"),
  fetch("/api/fallback"),
]);

事件循环与微任务

JavaScript 事件循环在宏任务 (setTimeout, setInterval) 之前处理微任务 (Promise 回调, queueMicrotask)。这就是 Promise 在超时之前解析的原因。异步迭代 (for await...of) 消费异步可迭代对象 —— 适用于流。异步生成器 (async function*) 产生异步可迭代对象,实现惰性异步序列。

typescript
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
// Output: 1, 2, 3, 4

// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));

// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
  for await (const chunk of stream) {
    console.log(chunk.length);
  }
}

// Async generators
async function* generate() {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

并发模式

这些模式在 TypeScript 中完全类型安全。防抖延迟执行直到调用停止 N 毫秒(搜索输入)。节流限制每 N 毫秒一次调用(滚动处理程序)。信号量/mapLimit 控制并发 —— 适用于限流 API。Parameters<T> 和 ReturnType<T> 在包装器中保留函数签名。

typescript
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Semaphore (limit concurrency)
async function mapLimit<T, U>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<U>
): Promise<U[]> {
  const results: U[] = [];
  const executing: Promise<void>[] = [];
  for (const item of items) {
    const p = fn(item).then(r => results.push(r));
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}
12

错误处理与测试

Try/Catch 与 Unknown

自 TypeScript 4.4 (useUnknownInCatchVariables) 起,捕获的错误为 'unknown' —— 使用前必须收窄。这防止访问不存在的属性。使用 instanceof 检查特定错误类型,或用 String() 作为后备。创建 getErrorMessage() 辅助函数以一致提取错误。

typescript
// In strict mode, catch is 'unknown' (not 'any')
try {
  JSON.parse("invalid");
} catch (error: unknown) {
  // Must narrow before using
  if (error instanceof SyntaxError) {
    console.error("JSON error:", error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

// Helper function
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

自定义错误类

自定义错误类向错误添加结构化数据(代码、状态码、字段)。始终调用 super(message) 并设置原型 (Object.setPrototypeOf) 以修复 TypeScript/ES5 原型链问题。在 catch 块中使用 instanceof 区分错误类型。此模式对 Express 中间件和 API 错误处理程序至关重要。

typescript
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "AppError";
    // Fix prototype chain (TS quirk)
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public field: string) {
    super(message, "VALIDATION_ERROR", 400);
    this.name = "ValidationError";
  }
}

// Usage
function createUser(input: unknown) {
  if (typeof input !== "object" || input === null) {
    throw new ValidationError("Invalid input", "body");
  }
}

try {
  createUser("bad");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.statusCode); // "body" 400
  }
}

Result 类型模式

Result 类型(来自 Rust)使错误在类型签名中显式 —— 调用者必须处理成功和失败。与异常不同,编译器强制错误处理。用于预期失败(验证、未找到),异常会过度。将异常保留给真正意外的错误(bug、系统故障)。

typescript
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
  console.log(result.value); // number
} else {
  console.error(result.error); // string
}

// Utility helpers
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

断言函数

断言函数 (asserts X) 在条件失败时抛出并在之后收窄类型。'asserts value is string' 告诉 TypeScript 调用后 value 是 string。这比重复 if 检查更清晰。用于边界处的运行时验证(API 输入、配置)。与 Zod 或 io-ts 结合进行 schema 验证。

typescript
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string

// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) throw new Error("undefined");
}

类型安全的 JSON 解析

JSON.parse 返回 'any' —— 不安全。用类型守卫包装以在运行时验证形状并收窄类型。对于复杂 schema,使用 Zod、io-ts 或 yup —— 它们从单个 schema 定义同时生成运行时验证器和 TypeScript 类型。这对 API 响应和用户输入至关重要。

typescript
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) return parsed;
    return null;
  } catch {
    return null;
  }
}

// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && typeof (x as User).id === "number"
    && typeof (x as User).name === "string";
}

const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
  console.log(data.name); // User
}

// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));

穷尽检查

穷尽检查确保处理联合的所有情况。将默认 case 赋值为 'never' —— 如果向联合添加新变体,TypeScript 会报错,因为新类型不可赋值给 'never'。这在编译时捕获遗漏情况。对区分联合、Redux reducer 和状态机至关重要。

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.w * shape.h;
    default:
      // If a new shape is added, this errors
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'
13

装饰器与元数据

类装饰器

类装饰器接收构造函数并可返回修改后的类。装饰器工厂(返回函数)接受参数。装饰器是实验性功能 —— 在 tsconfig 中启用 'experimentalDecorators'。在 NestJS、TypeORM 和 Angular 中大量用于依赖注入和元数据。

typescript
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Created ${target.name}`);
    }
  };
}

@Logged
class Service {
  constructor(public name: string) {}
}

const s = new Service("Auth");
// Logs: "Created Service"

// Decorator factory (with arguments)
function Prefix(prefix: string) {
  return function <T extends new (...args: any[]) => any>(target: T): T {
    return class extends target {
      message = prefix + " " + (this as any).name;
    };
  };
}

方法与属性装饰器

方法装饰器接收 (target, propertyKey, descriptor) 并可包装原始方法 —— 适用于日志、缓存和访问控制。属性装饰器接收 (target, key) 并常用于注册元数据。descriptor.value 是原始函数;包装它以添加行为。在 NestJS (@Get, @Post) 和 TypeORM (@Column) 中常见。

typescript
// Method decorator: (target, key, descriptor)
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}(${args})`);
    return original.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"

// Property decorator: (target, key)
function Required(target: any, key: string) {
  // Store metadata for validation
  console.log(`Required: ${key}`);
}

参数装饰器与元数据

参数装饰器接收 (target, key, index) 用于依赖注入 (NestJS, Angular)。'reflect-metadata' polyfill 启用运行时类型元数据 —— 装饰器可通过 Reflect.getMetadata('design:paramtypes') 访问参数类型。这是 DI 容器知道注入什么的方式。在 tsconfig 中启用 'emitDecoratorMetadata: true'。

typescript
import "reflect-metadata";

// Parameter decorator
function Inject(target: any, key: string, index: number) {
  console.log(`Inject param ${index} of ${key}`);
}

class Service {
  constructor(@Inject private dep: any) {}
}

// Store and retrieve metadata
const METADATA_KEY = "design:type";

class Example {
  greet(name: string): void {}
}

// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)

const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]

访问器装饰器

访问器装饰器应用于 getter/setter。descriptor 有可包装的 get/set 属性。用于验证、日志或更改可枚举性。验证模式 (MaxLength, Min, Max) 包装 setter 以在运行时强制约束。这是 class-validator (NestJS) 用于 DTO 验证的工作方式。

typescript
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
  return function (
    target: any,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string = "";

  @Enumerable(true)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)

// Validation decorator
function MaxLength(len: number) {
  return function (target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length > len) throw new Error("Too long");
        value = v;
      },
    });
  };
}

现代装饰器 (TC39 Stage 3)

TypeScript 5.0 支持 TC39 Stage 3 装饰器提案 —— 替代 experimentalDecorators 的标准化 API。新 API 使用上下文对象 (ClassMethodDecoratorContext) 而非 (target, key, descriptor)。它更清晰、类型安全,最终将成为 JS 标准。新项目使用此特性;experimental decorators 保留用于 NestJS/Angular 兼容性。

typescript
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class Service {
  @log
  greet(name: string): string {
    return `Hello, ${name}`;
  }
}

// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after construction

实用装饰器:Memoize

此 memoize 装饰器基于参数缓存方法结果 —— 对 fibonacci 等昂贵的纯函数显著加速。缓存是每实例的(使用 WeakMap 获得共享缓存)。装饰器适用于横切关注点:日志、缓存、验证、访问控制、重试逻辑。它们通过分离基础设施关注点保持业务逻辑清晰。

typescript
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, Return>();
  return function (this: This, ...args: Args): Return {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n < 2) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");

console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");
14

工具类型

Partial、Required 与 Readonly

Partial<T> 使所有属性可选 —— 非常适合只有部分字段更改的更新/修补操作。Required<T> 是反向操作。Readonly<T> 使所有属性在编译时不可变。这些是最常用的工具类型,消除了手动维护并行可选/只读接口的需要。

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

function updateUser(id: number, changes: UserUpdate) {
  // Only provided fields are updated
  Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name

// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required

// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly

// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutation

Pick、Omit 与 Record

Pick<T, K> 提取属性子集;Omit<T, K> 移除属性 —— 两者都创建派生类型而无需重复。Record<K, V> 创建带特定键的映射/字典类型。这些对 DTO(数据传输对象)至关重要:通过省略 id 和 createdAt 等自动生成字段从 User 派生 CreateUser 类型。这保持类型 DRY 且同步。

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }

// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;

ReturnType、Parameters 与 Awaited

ReturnType 和 Parameters 从现有函数提取类型 —— 在包装或调用不想重复签名的函数时非常有用。Awaited<T> 解包嵌套 Promise (Promise<Promise<T>> 变为 T),对异步函数返回类型至关重要。InstanceType 从类构造函数获取实例类型。这些实现类型安全的函数组合和高阶工具。

typescript
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>

// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }

// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number

// ConstructorParameters<T>: parameters of a class constructor
class Point {
  constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]

// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // Point

Exclude、Extract 与 NonNullable

Exclude<T, U> 从联合中移除类型;Extract<T, U> 只保留匹配类型 —— 两者都操作联合成员。NonNullable<T> 去除 null 和 undefined。这些是构建块:Omit 定义为 Pick<T, Exclude<keyof T, K>>。使用 Exclude/Extract 动态过滤联合类型,例如从结果联合中分离错误类型和成功类型。

typescript
type Role = "admin" | "user" | "guest" | "superadmin";

// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"

// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"

// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Practical: filter union types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"

// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

自定义工具类型

自定义工具类型为特定需求组合内置类型。Optional<T, K> 只使某些字段可选(比 Partial 更有针对性)。DeepPartial/DeepReadonly 递归应用于嵌套对象 —— 适用于配置和状态树。Mutable 中的 -readonly 修饰符移除 readonly。这些模式展示映射类型和条件类型如何结合实现强大的类型级编程。

typescript
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }

// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Mutable (remove readonly from all properties)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
  [K in keyof T]: T[K] extends never ? never : K;
}[keyof T];
15

条件类型

基本条件类型 (T extends U ? X : Y)

条件类型 (T extends U ? X : Y) 基于类型级条件选择类型 —— 类似类型的三元运算符。它们是 TypeScript 类型级编程的基础。当 T 是联合时,条件在每个成员上分发(分发条件类型)。infer 关键字从模式内提取类型,如从数组中提取元素类型或从 Promise 中提取解析类型。

typescript
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y

// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (not an array)

// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<number>>;  // number
type E = UnwrapPromise<string>;           // string (not a promise)

// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<number>;      // false

// Conditional types are evaluated lazily and distribute over unions

infer 关键字(类型提取)

infer 关键字在条件类型的 extends 子句内声明类型变量,捕获匹配该位置的任何类型。这是 ReturnType、Parameters 和 Awaited 的实现方式。infer 可递归使用 (Unwrap<Promise<Promise<T>>>) 完全解包嵌套类型。它是从复杂泛型结构中提取类型的主要工具。

typescript
// infer extracts a type from within a pattern

// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string

// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)

// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number

// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;

// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;

分发条件类型

条件类型在联合上分发:应用 ToArray<A | B> 得到 ToArray<A> | ToArray<B>,而非 (A | B)[]。这是 Exclude 和 NonNullable 过滤联合成员的方式 —— 它们对排除的类型返回 'never',在联合中坍缩。为防止分发,用方括号包裹两侧:[T] extends [U]。分发通常用于过滤,但'包装整个联合'操作需要非分发。

typescript
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y  applied to  A | B  becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)

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

type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]

// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]  — a single array of the union

// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number  (null and undefined filtered out)

// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;

条件类型约束

条件类型可嵌套创建类型级区分(类似类型的 switch 语句)。与 infer 结合,从泛型参数提取和派生类型。这是 React 等库从组件定义派生 prop 类型的方式,也是路由库从路径字符串提取参数类型的方式。约束 (T extends any[]) 确保输入有效后条件才提取元素类型。

typescript
// Use conditional types to constrain and derive types

// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type

// Conditional with multiple branches (nested)
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>;         // "object"

// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
  return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // number

模板字面量类型(字符串操作)

模板字面量类型实现类型级字符串操作 —— 拼接、大小写转换和模式匹配。与条件类型和 infer 结合,可解析路径字符串提取路由参数、生成事件处理程序名或构建类型安全的属性访问器。这是 Next.js 和 tRPC 等框架从字符串字面量创建端到端类型安全 API 的方式。

typescript
// Template literal types: type-level string operations

type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">;    // "Foo"

// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Extract route parameters
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}/${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}/${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"

// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T ? Get<T[Key], Rest> : never
    : P extends keyof T ? T[P] : never;
16

映射类型

基本映射类型

映射类型遍历对象的键并转换每个属性 —— [K in keyof T] 是语法。Partial、Readonly、Pick 等工具类型就是这样实现的。可修改属性类型 (T[K] | null)、添加修饰符 (? 或 readonly),或完全替换值类型。映射类型是 TypeScript 类型转换系统的骨干。

typescript
// Mapped types transform each property of an existing type

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

// Change all property types to a specific type
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }

通过 'as' 重映射键

键重映射 (as 子句, TS 4.1+) 允许在映射期间重命名或过滤键。使用模板字面量类型转换键名(添加前缀、转换为 getter、大写)。为键返回 'never' 会移除它 —— 这是过滤属性的方式。与条件类型结合,键重映射实现强大转换,如将数据 schema 转换为验证 schema 或 API 类型转换为表单类型。

typescript
// Remap keys using 'as' (TypeScript 4.1+)

// Add a prefix to all keys
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  id: number;
  name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }

// Convert keys to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Filter out keys by returning never
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }

// Make keys UPPERCASE
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

修饰符:+、-、?、readonly

+ 和 - 修饰符添加或移除属性修饰符。-? 移除可选性(使可选字段变为必需);-readonly 移除不可变性。这是 Required<T> 和 Mutable 模式的工作方式。+ 前缀是可选的 (readonly 与 +readonly 相同),但 - 是移除所必需的。这在类型转换期间对属性特性提供细粒度控制。

typescript
// Add (+) or remove (-) modifiers

// Remove optional (?) modifier: -?
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface OptionalUser {
  id?: number;
  name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now

// Remove readonly modifier: -readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface FrozenConfig {
  readonly port: number;
  readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now

// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
  +readonly [K in keyof T]: T[K];
};

// Add optional modifier: +?
type MakeOptional<T> = {
  [K in keyof T]+?: T[K];
};

同态映射类型

同态映射类型 ([K in keyof T]) 保留源类型的属性修饰符 (readonly, ?) —— 这就是 Pick<User, 'id'> 保持 id 为 readonly 的原因。非同态映射(如 [K in string])不保留修饰符。这在派生类型时很重要:带 readonly 字段的类型的同态 Partial 保持这些字段为 readonly(但可选)。理解同态有助于预测修饰符是否在转换后保留。

typescript
// Homomorphic mapped types preserve modifiers from the original type

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
  [K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User

// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
  [K in string]: T; // not keyof T — loses modifiers
};

// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!

// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
  [K in keyof T]?: T[K]; // Partial is homomorphic
};

从 Schema 构建验证类型

这是表单库 (React Hook Form, Formik) 和验证库 (Zod, Yup) 保持类型安全的方式 —— 它们使用映射类型从数据接口派生验证器和表单类型。向 User 添加字段时,验证器和表单类型也自动需要它,防止漂移。这展示了映射类型的实际威力:单一真相源(接口)驱动多个派生类型。

typescript
// Practical: derive a validator type from a data type

// Given a data interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Create a validator type where each field is a validation function
type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean;
};

const userValidator: Validator<User> = {
  id: (v) => v > 0,
  name: (v) => v.length > 0,
  email: (v) => v.includes("@"),
};

// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type Form<T> = {
  [K in keyof T]: FormField<T[K]>;
};

const userForm: Form<User> = {
  id: { value: 1, error: null, touched: false },
  name: { value: "Alice", error: null, touched: true },
  email: { value: "", error: "Required", touched: true },
};

// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.
17

类型守卫与收窄

typeof 与 instanceof 收窄

TypeScript 基于运行时检查收窄类型。typeof 收窄基本类型 (string, number, boolean 等);instanceof 收窄类实例。真值检查 (if (value)) 排除 null/undefined/0/''/false。收窄适用于条件成立的分支。这是 TypeScript 使运行时检查携带类型信息的方式,消除显式转换的需要。

typescript
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
    // padding is narrowed to 'number' here
  }
  return padding + value;
  // padding is narrowed to 'string' here
}

// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow(); // OK — narrowed to Cat
  } else {
    animal.bark(); // OK — narrowed to Dog
  }
}

// typeof returns: "string" | "number" | "boolean" | "symbol"
//   "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)

// Truthiness narrowing
function process(value?: string) {
  if (value) {
    console.log(value.toUpperCase()); // value is string (not undefined)
  }
}

in 运算符与区分联合

'in' 运算符基于属性存在性收窄。区分联合使用共享字面量属性(如 'kind' 或 'type')作为标签 —— switch 它收窄到正确变体并访问完整属性。这是 TypeScript 中和类型/代数数据类型的等价物。是 Redux action、状态机和多形状 API 响应的标准模式。始终为判别式使用字面量类型。

typescript
// 'in' operator checks for a property — narrows to types that have it

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // narrowed to Fish
  } else {
    animal.fly(); // narrowed to Bird
  }
}

// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed by kind
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)

自定义类型守卫函数

自定义类型守卫(返回类型 'x is Type')将复杂运行时检查封装到收窄类型的可复用函数中。断言函数 (asserts x is T) 抛出而非返回布尔值 —— 它们在调用后的所有代码中收窄。使用类型守卫验证不可信数据 (JSON.parse, API 响应) 并将其带入类型系统。这桥接运行时验证和编译时类型之间的差距。

typescript
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true

interface User {
  id: number;
  name: string;
}

function isUser(obj: any): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');

if (isUser(data)) {
  console.log(data.name); // data is narrowed to User
}

// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
  return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}

// Type guard for discriminated unions
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
  return r.success;
}

// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Expected value to be defined");
  }
}

用 never 进行穷尽检查

穷尽检查使用 'never' 类型确保处理所有联合变体。如果向联合添加新变体但忘记 case,默认分支的 'never' 赋值变为编译错误。assertNever 辅助函数在运行时抛出并在编译时报错遗漏情况。这是区分联合最有价值的模式 —— 它让编译器告诉你何时忘记了 case。

typescript
// The 'never' type ensures all cases are handled

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If we add a new shape but forget a case, this line
      // becomes a compile error because 'shape' would not be 'never'
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'

// Helper function pattern
function assertNever(x: never): never {
  throw new Error("Unexpected: " + JSON.stringify(x));
}

function process(shape: Shape) {
  switch (shape.kind) {
    case "circle": return;
    case "square": return;
    default: return assertNever(shape); // compile error if a case is missing
  }
}

用数组方法收窄

Array.filter 默认不收窄元素类型,因为其回调返回布尔值而非类型守卫。要收窄,传递自定义类型守卫函数 (pet is Dog) —— 然后 filter 返回收窄的数组类型。TypeScript 也在回调体 (forEach, map) 中基于 if 检查收窄。Array.isArray 是将 unknown/any 收窄为数组类型的内置类型守卫。

typescript
// TypeScript narrows through filter, map, and control flow

interface Pet {
  name: string;
  speak(): void;
}

class Dog implements Pet {
  name = "Rex";
  speak() { console.log("Woof"); }
  fetch() { console.log("Fetching!"); }
}

class Cat implements Pet {
  name = "Whiskers";
  speak() { console.log("Meow"); }
}

const pets: Pet[] = [new Dog(), new Cat(), new Dog()];

// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
  return pet instanceof Dog;
}

const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog

// Narrowing in forEach/callbacks
pets.forEach((pet) => {
  if (isDog(pet)) {
    pet.fetch(); // narrowed to Dog inside callback
  }
});

// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
  if (Array.isArray(input)) {
    input.length; // OK — input is any[] (or unknown[])
  }
}
18

类型推断

变量与返回类型推断

TypeScript 从初始化器和返回语句推断类型,因此很少需要显式注解。变量扩展为其一般类型 (let x = 10 推断为 number 而非 10)。'as const' 防止扩展:使字面量保持字面量、对象只读、数组变为只读元组。typeof colors[number] 提取元组元素类型的联合 —— 从数组派生类枚举类型的常见模式。

typescript
// TypeScript infers types when you don't annotate

// Variable inference
let count = 0;          // number
let name = "Alice";     // string
let items = [1, 2, 3];  // number[]
let mixed = [1, "two"]; // (string | number)[]

// Function return type inference
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// const assertions for literal types
const x = 10;        // number (widened)
const y = "hello";   // string (widened)
const z = { a: 1 };  // { a: number }

const x2 = 10 as const;       // 10 (literal type)
const y2 = "hello" as const;  // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }

// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"

上下文类型化

上下文类型化将预期类型向后流入表达式。将函数赋值给类型化变量时,参数类型从目标类型推断。这就是事件处理程序、数组回调和对象字面量通常不需要类型注解的原因。经验法则:注解函数签名(公共 API 的参数和返回类型),但让推断处理局部变量和回调。

typescript
// Contextual typing: the expected type influences the inferred type

// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
  // mouseEvent is inferred as MouseEvent (from onmousedown's type)
  console.log(mouseEvent.button);
};

// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]

// Contextual typing for object literals
interface User {
  name: string;
  age: number;
}
const user: User = {
  name: "Alice", // inferred as string (from User.name)
  age: 30,
};

// Contextual typing with discriminated unions
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; scrollTop: number };

function handle(e: Event) {
  if (e.type === "click") {
    console.log(e.x, e.y); // narrowed
  }
}

// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation needed

最佳公共类型(联合推断)

从多个值推断时(如数组字面量),TypeScript 找到'最佳公共类型' —— 通常是超类型或联合。[Dog, Cat] 数组推断为 Animal[](公共基类),而非 (Dog | Cat)[]。要获得联合,显式注解。条件返回推断所有分支的联合。理解这一点有助于预测何时需要显式注解 vs 推断足够。

typescript
// When inferring from multiple values, TS finds the best common type

// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]

// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]

// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }

const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])

// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];

// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]

// Return type inference with conditionals
function getValue(flag: boolean) {
  return flag ? 42 : "hello"; // inferred as number | string
}

控制流分析

TypeScript 执行控制流分析 —— 跟踪类型如何通过 if/else、return、赋值和逻辑运算符收窄和扩展。类型在检查后收窄并保持收窄直到变量重新赋值。提前返回(保护子句)特别有效:'if (value === null) return' 后,函数其余部分知道 value 不是 null。这就是保护子句风格代码与 TypeScript 配合良好的原因。

typescript
// TypeScript tracks types through control flow

function example(value: string | number | null) {
  // After this check, 'value' is string | number in this branch
  if (value === null) {
    return;
  }

  // value is now string | number (null narrowed out)
  console.log(value);

  if (typeof value === "string") {
    return value.toUpperCase(); // value is string
  }

  // value is now number (string and null narrowed out)
  return value.toFixed(2);
}

// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number

// Narrowing with && and ||
function process(input?: string) {
  const value = input && input.trim(); // string | undefined
  const safe = input || "default"; // string (undefined narrowed out)
}

// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCase

satisfies 运算符 (TS 4.9+)

'satisfies' 运算符 (TS 4.9+) 验证值符合类型同时保留最具体的推断类型 —— 不同于类型注解会扩展。这非常适合配置、路由映射和主题对象:获得结构正确的编译时验证,但属性访问仍返回精确的字面量类型。与 'as const' 结合同时获得字面量保留和结构验证。

typescript
// 'satisfies' checks a value matches a type WITHOUT widening it

// Problem: annotation widens the type
const config1: Record<string, string | number> = {
  port: 8080,
  host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)

// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
  port: 8080,
  host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated

// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;

const routes = {
  "/users": { method: "GET", handler: () => fetchUsers() },
  "/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;

routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)

// 'as const' + 'satisfies': literal types + validation
const colors = {
  primary: "#ff0000",
  secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;
19

声明文件与模块增强

编写 .d.ts 声明文件

.d.ts 文件包含类型声明(无实现)—— 描述 JavaScript 代码的类型。使用 'declare module' 为无类型 npm 包添加类型。'declare global' 扩展 Window 等全局类型。环境声明告诉 TypeScript '这在运行时存在,相信我'。这是将遗留 JS、浏览器 API 和构建时注入变量集成到类型系统的方式。

typescript
// types/my-module.d.ts — describes the types of a JS library

// Declare a module (for libraries without types)
declare module "untyped-lib" {
  export function greet(name: string): string;
  export const version: string;
  export interface Config {
    timeout: number;
    retries: number;
  }
}

// Ambient declarations for global variables
declare global {
  interface Window {
    myCustomProp: string;
    myApp: { init: () => void };
  }
}

// Now window.myCustomProp is typed
// window.myCustomProp; // string

// Declaring a global function
declare function myGlobalFn(x: number): string;

// Declaring a global namespace
declare namespace MyLib {
  function doSomething(): void;
  const version: string;
}

// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constants

模块增强(扩展现有类型)

模块增强从其他模块扩展现有类型 —— 向接口添加属性而无需修改原始源。这是 Express 中间件(如 passport)添加 req.user 的方式,也是扩展第三方库类型的方式。'declare module' 语法重新打开模块的类型空间。增强必须在模块中(带 import/export 的文件)才能全局生效。

typescript
// Module augmentation: add to an existing module's types

// Extend an interface from another module
import express from "express";

declare module "express" {
  interface Request {
    user?: {
      id: number;
      name: string;
    };
    // Now req.user is typed on all Express requests
  }
}

// Augment a third-party module
declare module "axios" {
  export interface AxiosRequestConfig {
    retryCount?: number; // add a custom config option
  }
}

// Augment a global interface
declare global {
  interface Array<T> {
    last(): T | undefined; // add a custom array method
  }
}

// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — now typed!

// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.

三斜杠指令

三斜杠指令 (///) 是指示 TypeScript 包含额外文件或类型包的特殊编译器注释。最常见的是 /// <reference types='node' /> 用于包含 @types/node。使用现代 tsconfig.json 的 'types' 和 'lib' 选项时很少需要 —— 优先使用基于配置的设置。它们主要出现在 .d.ts 文件和遗留代码中。理解它们有助于阅读声明文件。

typescript
// Triple-slash directives are special comments processed by TS

/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)

/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)

/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")

// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />

declare function readFile(path: string): Buffer; // Buffer from @types/node

// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.

// Prefer tsconfig.json settings:
// {
//   "compilerOptions": {
//     "types": ["node"],      // instead of /// <reference types="node" />
//     "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
//   }
// }

随包发布类型

要随 npm 包发布 TypeScript 类型,在 package.json 中设置 'types' 指向 .d.ts 文件并在 tsconfig 中启用 'declaration: true'。消费者安装包时自动获得类型。declarationMap 启用'转到定义'跳转到源 .ts 文件。对于没有捆绑类型的库,DefinitelyTyped 项目 (@types/package) 提供社区维护的声明。

typescript
// package.json for a library with TypeScript types
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  // <-- points to the type declarations
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

// tsconfig.json for building a library
{
  "compilerOptions": {
    "declaration": true,        // generate .d.ts files
    "declarationMap": true,     // generate .d.ts.map (source maps for types)
    "sourceMap": true,          // generate .js.map
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true           // enable project references
  }
}

// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!

// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express

// Check if types exist: https://www.typescriptlang.org/dt/search

仅类型导入与导出

'import type' 仅导入类型信息 —— 在运行时完全擦除,减小包大小并避免循环依赖问题。用于接口、类型别名和仅类型重导出。内联 'import { x, type Y }' 语法 (TS 4.5+) 干净地混合值和类型导入。verbatimModuleSyntax (TS 5.0+) 严格强制此行为。仅在类型位置使用的内容优先使用 'import type'。

typescript
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";

// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies

// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only

// 'export type' re-exports types only
export type { User, Config } from "./types";

// Interface vs Type for declarations
interface IUser { name: string; }  // can be augmented/merged
type TUser = { name: string };     // cannot be merged, more flexible

// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
//   "compilerOptions": { "verbatimModuleSyntax": true }
// }
20

tsconfig.json 选项

核心编译器选项

tsconfig.json 控制 TypeScript 如何编译。'target' 设置输出 JS 版本;'module' 设置模块系统。'strict: true' 是最重要的单一设置 —— 它启用所有严格类型检查 (noImplicitAny, strictNullChecks 等)。'lib' 确定哪些内置 API 可用 (DOM 用于浏览器,ES2022 用于现代 JS 特性)。新项目始终以 strict: true 开始。

typescript
{
  "compilerOptions": {
    "target": "ES2022",        // JS version to compile to
    "module": "ESNext",        // module system (ESNext, CommonJS, etc.)
    "moduleResolution": "bundler", // how modules are resolved
    "lib": ["ES2022", "DOM"],  // available type definitions
    "outDir": "./dist",        // output directory for compiled JS
    "rootDir": "./src",        // root of source files
    "sourceMap": true,         // generate .map files for debugging
    "declaration": true,       // generate .d.ts files (for libraries)
    "removeComments": true,    // strip comments from output

    // Type checking strictness
    "strict": true,            // enable ALL strict checks (recommended)
    "noImplicitAny": true,     // error on implicit 'any'
    "strictNullChecks": true,  // null/undefined are separate types
    "noUnusedLocals": true,    // error on unused local variables
    "noUnusedParameters": true,// error on unused function params
    "noImplicitReturns": true, // error if not all paths return
    "noFallthroughCasesInSwitch": true,

    "esModuleInterop": true,   // allow default imports from CommonJS
    "skipLibCheck": true,      // skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true
  }
}

严格模式标志详解

严格模式是严格性标志的集合。strictNullChecks 影响最大 —— 它使 null/undefined 成为不同类型,强制显式处理(运行时崩溃的头号来源)。noImplicitAny 防止静默类型侵蚀。strictPropertyInitialization 捕获未初始化的类字段(用 ! 表示确定赋值或在构造函数中初始化)。新项目始终启用严格模式 —— 前期成本值得安全。

typescript
{
  "compilerOptions": {
    // 'strict: true' enables ALL of these:

    "strictNullChecks": true,
    // null and undefined are NOT assignable to other types
    // without explicit union. Forces null handling.
    let x: string = null; // ERROR (without this, it's allowed)

    "noImplicitAny": true,
    // Parameters/variables can't be implicitly 'any'
    function fn(x) { } // ERROR: x is implicitly any

    "strictFunctionTypes": true,
    // Function parameter types checked contravariantly
    // (catches unsafe function assignments)

    "strictBindCallApply": true,
    // bind/call/apply are type-checked

    "strictPropertyInitialization": true,
    // Class properties must be initialized or declared with !
    class User {
      name: string; // ERROR: not initialized
      name2!: string; // OK: definite assignment assertion
    }

    "noImplicitThis": true,
    // 'this' must have a known type (no implicit any)

    "alwaysStrict": true
    // Emit "use strict" in every file
  }
}

模块解析策略

moduleResolution 控制导入路径如何解析。'node' 是经典策略;'bundler' (TS 5.0+) 匹配 Vite 等现代打包器并支持 package.json exports。'nodenext' 是严格 ESM(需要扩展名)。paths 创建导入别名 (@/ → src/),必须在打包器配置中镜像(如 Vite 的 resolve.alias)。baseUrl + paths 是避免深层相对导入 (../../../) 的标准方式。

typescript
{
  "compilerOptions": {
    // How TS resolves import paths

    "moduleResolution": "node",    // classic Node.js resolution
    // Looks for: file.ts, file/index.ts, node_modules/file

    "moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
    // Matches how bundlers resolve: supports import maps,
    // conditional exports, no file extension requirement

    "moduleResolution": "nodenext", // Node.js ESM resolution (strict)
    // Requires file extensions in imports: import "./foo.js"

    // Path mapping (aliases)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],           // import "@/components/Button"
      "@utils/*": ["src/utils/*"],
      "@components": ["src/components/index.ts"]
    }

    // rootDirs: virtual directories that map to the same location
    "rootDirs": ["src", "generated"],
    // imports between src/ and generated/ resolve as if same dir
  }
}

// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.ts

项目引用(Monorepo)

项目引用将大型代码库拆分为独立编译的子项目 —— 对 monorepo 至关重要。每个项目有 composite: true 并发出声明。引用声明项目间依赖。tsc --build (-b) 按依赖顺序编译,只重建更改的部分。这大幅加速大型代码库的类型检查并强制包间的架构边界。

typescript
// tsconfig.json (root — references sub-projects)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,          // required for project references
    "declaration": true,        // must emit declarations
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { /* ... */ },
  "references": [
    { "path": "../shared" }     // depends on shared
  ]
}

// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically

// Build command: tsc --build (or tsc -b)

常用 tsconfig 配方

不同项目类型需要不同 tsconfig 设置。React/Vite 使用 jsx: 'react-jsx' 和 noEmit (Vite 编译)。Node.js 使用 CommonJS(或 ESM 用 NodeNext)和 types: ['node']。库需要 declaration: true 输出 .d.ts 和较低 target 以获得更广泛兼容性。isolatedModules 是 Vite/esbuild 所需的(每个文件必须可独立编译)。始终从构建中排除测试文件和 node_modules。

typescript
// React + Vite project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",          // React 17+ JSX transform
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,             // Vite handles compilation
    "isolatedModules": true,    // required by Vite/esbuild
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

// Node.js backend project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",        // or NodeNext for ESM
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "types": ["node"],           // @types/node
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

// Library project (publishes to npm)
{
  "compilerOptions": {
    "target": "ES2020",          // broader compatibility
    "module": "ESNext",
    "declaration": true,         // emit .d.ts
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
21

装饰器

类装饰器

类装饰器接收构造函数并可返回修改后的类。它们是实验性的(需要 experimentalDecorators: true)。在 NestJS 和 TypeORM 中常见。

typescript
function logged<T extends new (...args: any[]) => any>(con: T) {
    return class extends con {
        created = new Date().toISOString();
    };
}
@logged
class Service { constructor(public name: string) {} }

方法装饰器

方法装饰器接收 (target, key, descriptor)。包装 descriptor.value 启用日志、缓存、验证。这是 NestJS 拦截器的工作方式。

typescript
function log(target: any, key: string, desc: PropertyDescriptor) {
    const orig = desc.value;
    desc.value = function(...args: any[]) {
        console.log(`Calling ${key}`, args);
        return orig.apply(this, args);
    };
}
class Calc { @log add(a: number, b: number) { return a + b; } }

属性装饰器

属性装饰器接收 (target, key)。使用 Object.defineProperty 创建 getter/setter 进行验证。在 class-validator 中用于 DTO 验证。

typescript
function required(target: any, key: string) {
    let val: any;
    Object.defineProperty(target, key, {
        get() { return val; },
        set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
    });
}

参数装饰器

参数装饰器接收 (target, methodKey, parameterIndex)。与元数据反射一起用于验证。class-validator 和 NestJS 使用此特性。

typescript
function Min(min: number) {
    return (target: any, key: string, idx: number) => {
        console.log(`${key} param ${idx} >= ${min}`);
    };
}
class Order { create(@Min(0) qty: number) { return qty; } }

装饰器工厂

装饰器工厂返回装饰器函数,启用配置。外部函数接收参数,内部是实际装饰器。

typescript
function Retry(times: number) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
        const orig = desc.value;
        desc.value = async function(...args: any[]) {
            for (let i = 0; i < times; i++)
                try { return await orig.apply(this, args); }
                catch (e) { if (i === times - 1) throw e; }
        };
    };
}
22

模块增强

增强内置类型

模块增强扩展现有类型。declare global 允许增强 Array 等内置类型。还必须提供运行时实现。

typescript
declare global {
    interface Array<T> {
        last(): T | undefined;
        chunk(size: number): T[][];
    }
}
Array.prototype.last = function() { return this[this.length - 1]; };

增强库类型

模块增强从第三方库扩展类型。declare module 重新打开模块类型。对向框架对象添加自定义属性至关重要。

typescript
declare module 'express' {
    interface Request {
        user?: { id: string; role: string };
    }
}
app.get('/profile', (req, res) => {
    const userId = req.user?.id;  // Typed!
});

增强 Window

增强 Window 添加带类型安全的自定义全局属性。适用于向调试工具或分析暴露应用状态。

typescript
declare global {
    interface Window {
        myApp: { init: () => void; version: string };
    }
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };

CSS 模块

CSS 模块需要类型声明。声明将 .module.css 导入映射到类名记录。启用 CSS 类引用的自动补全。

typescript
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />

Vue 插件

Vue 和其他框架使用模块增强进行插件类型化。ComponentCustomProperties 添加实例属性。启用类型安全的插件。

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

声明合并

合并接口

同名接口自动合并。所有成员成为单个接口的一部分。适用于跨文件拆分接口。

typescript
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };

合并命名空间

同名命名空间合并其导出。这允许跨文件拆分命名空间内容。新代码优先使用 ES 模块。

typescript
namespace Utils {
    export function log(msg: string) { console.log(msg); }
}
namespace Utils {
    export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');

命名空间与函数

命名空间可与函数、类和枚举合并。命名空间向函数添加静态属性。在 Moment.js 等库中使用。

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

与类合并

命名空间与类合并添加静态成员和嵌套类型。命名空间可导出成为嵌套类型的接口。

typescript
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
    export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };

不允许的合并

类不能与其他类合并。变量不能合并。函数作为重载合并。枚举可与命名空间合并。

typescript
// Cannot merge classes
class A { x = 1; }
class A { y = 2; }  // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }
24

类型收窄

typeof 与 instanceof

typeof 收窄基本类型。instanceof 收窄类类型。TypeScript 理解这些检查并在每个分支中收窄类型。

typescript
function process(value: string | number | Date) {
    if (typeof value === 'string') return value.toUpperCase();
    if (typeof value === 'number') return value.toFixed(2);
    if (value instanceof Date) return value.toISOString();
}

in 运算符

in 运算符检查属性是否存在,收窄类型。适用于具有不同属性名的区分联合。

typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
    if ('meow' in animal) animal.meow();
    else animal.bark();
}

区分联合

区分联合使用公共属性(判别式)收窄类型。switch 判别式进行穷尽检查。变体类型的最安全模式。

typescript
type Result =
    | { status: 'success'; data: string }
    | { status: 'error'; message: string };
function handle(r: Result) {
    switch (r.status) {
        case 'success': console.log(r.data); break;
        case 'error': console.log(r.message); break;
    }
}

类型谓词

类型谓词 (x is T) 启用自定义收窄函数。返回 true 收窄为 T,false 收窄为排除类型。TypeScript 盲目信任谓词。

typescript
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
    if (isFish(pet)) pet.swim();
    else pet.fly();
}

断言函数

断言函数在条件失败时抛出,为后续代码收窄类型。asserts x is T 收窄为 T。消除冗余的空检查。

typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
    if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
    assertDefined(user);
    console.log(user.name);  // User (not undefined)
}
25

模板字面量类型

基本模板字面量

模板字面量类型创建字符串模式。它们约束字符串匹配模板。为 API 端点和事件名启用类型安全模式。

typescript
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world';  // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';

Uppercase 与 Lowercase

内置内在类型转换字符串字面量类型。与模板字面量结合生成类型安全的事件名和常量。

typescript
type Upper = Uppercase<'hello'>;  // 'HELLO'
type Lower = Lowercase<'WORLD'>;  // 'world'
type Cap = Capitalize<'foo'>;     // 'Foo'
type EventName = `on${Capitalize<'click'>}`;  // 'onClick'

键重映射

键重映射 (as 子句) 在映射类型期间转换键。从属性名生成 getter/setter 名。从接口创建类型安全 API。

typescript
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

字符串模式匹配

带 infer 的模板字面量类型可在编译时解析字符串。Split 将字符串拆分为元组。启用类型安全的字符串操作。

typescript
type Split<S extends string, D extends string> =
    S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>;  // ['a', 'b', 'c']

事件系统类型化

带泛型的模板字面量类型创建完全类型安全的事件系统。事件名确定负载类型。on 和 emit 强制匹配类型。

typescript
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
    on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x));  // Typed!
26

infer 关键字

提取返回类型

infer 在条件类型内声明类型变量。它捕获特定位置的类型。ReturnType 是内置等价物。

typescript
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>;  // { name: string; age: number; }

提取 Promise 类型

infer 提取 Promise 的内部类型。DeepUnwrap 递归解包嵌套 Promise。内置 Awaited<T> 在现代 TypeScript 中执行此操作。

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>;  // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>;  // boolean

提取数组元素

infer E 捕获数组的元素类型。对于元组,infer 可捕获特定位置。适用于处理泛型集合。

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>;  // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>;  // string

提取函数参数

infer P 捕获函数的参数元组。Parameters 是内置等价物。适用于包装函数同时保留类型。

typescript
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>;  // [string, number]

多个 infer

多个 infer 变量可同时捕获类型的不同部分。在单个条件中启用复杂类型转换。

typescript
type FirstLast<T extends any[]> =
    T extends [infer First, ...any[], infer Last]
        ? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>;  // { first: 1; last: 4 }
27

协变

协变

协变允许 Dog[] 赋值给 Animal[]。TypeScript 数组是协变的但这不健全:将 Animal 推入 Dog[] 会损坏数组。

typescript
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs;  // OK (covariant)
// animals.push(new Animal());  // Unsafe at runtime

逆变

逆变意味着接受 Dog 的函数可用于期望接受 Animal 函数的地方。安全,因为 Dog 处理程序处理任何是 Dog 的 Animal。

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

双变

方法语法是双变的。函数属性语法在 strictFunctionTypes 下是逆变的。方法为 OO 兼容性而双变。

typescript
interface IFace {
    method(x: Animal): void;  // bivariant
    fn: (x: Animal) => void;  // contravariant
}
class Impl implements IFace {
    method(x: Dog) {}  // OK (bivariant)
    fn = (x: Dog) => {}  // Error (contravariant)
}

in/out 协变

TypeScript 4.7+ 支持显式协变注解。in 标记逆变(消费者),out 标记协变(生产者),in out 标记不变(两者)。

typescript
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }

不变类型

不变类型需要精确类型匹配。当类型同时出现在输入和输出位置时是不变的。Box<Dog> 不能赋值给 Box<Animal>。

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

构建器模式

流式构建器

构建器模式逐步构建复杂对象。每个方法返回 this 以链式调用。适用于 SQL 查询、HTTP 请求和配置。

typescript
class QueryBuilder {
    private parts: string[] = [];
    select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
    from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
    where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
    build(): string { return this.parts.join(' '); }
}

类型安全构建器

类型安全构建器使用条件类型强制必需字段。build() 仅在 hasName 为 true 时返回 Person。在编译时捕获遗漏字段。

typescript
interface State { hasName: boolean; }
class Builder<S extends State> {
    name(n: string): Builder<S & { hasName: true }> { return this as any; }
    build(): S extends { hasName: true } ? Person : never { return {} as any; }
}

不可变构建器

不可变构建器为每次修改创建新实例。类型系统通过交叉类型跟踪所有添加的键。每个 set 返回新的构建器类型。

typescript
class ImmutableBuilder<T extends object> {
    constructor(private data: T) {}
    set<K extends string, V>(key: K, value: V):
        ImmutableBuilder<T & { [P in K]: V }> {
        return new ImmutableBuilder({ ...this.data, [key]: value });
    }
    build(): T { return this.data; }
}

导演模式

导演封装常见构建序列。它使用构建器创建标准产品。不同导演产生不同变体。

typescript
class HTMLBuilder {
    private html = '';
    addTag(tag: string, content: string): this {
        this.html += `<${tag}>${content}</${tag}>`; return this;
    }
    build(): string { return this.html; }
}
class Director {
    buildPage(title: string, body: string): string {
        return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
    }
}

步骤构建器

步骤构建器通过类型系统强制特定方法调用顺序。每步返回只有下一个方法可用的不同类型。

typescript
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
    constructor(private name: string) {}
    age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
    constructor(private name: string, private age: number) {}
    build() { return { name: this.name, age: this.age }; }
}
29

TypeScript 测试

Jest 与 TypeScript

使用 ts-jest 或 @swc/jest 进行 TypeScript 测试。describe 分组相关测试,it 定义测试用例。expect 创建带 toBe、toEqual 等匹配器的断言。

typescript
import { sum } from './sum';
describe('sum', () => {
    it('adds two numbers', () => {
        expect(sum(1, 2)).toBe(3);
    });
    it('handles negatives', () => {
        expect(sum(-1, -2)).toBe(-3);
    });
});

类型测试

expectTypeOf 在编译时测试类型。验证返回类型、参数类型和解析的 Promise 类型。类型错误时构建失败。

typescript
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
    const fn = {} as unknown as GetUser;
    expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
    expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});

带类型的 Mock

jest.Mocked<T> 从接口创建类型化 mock。jest.fn() 创建带类型化返回值的 mock 函数。mock 完全类型化。

typescript
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
    find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });

测试设置与拆卸

beforeAll 在所有测试前运行一次,afterAll 在之后一次。beforeEach 在每个测试前运行,afterEach 在之后。用于设置和清理。

typescript
describe('Database', () => {
    beforeAll(async () => { db = createDatabase(); await db.connect(); });
    afterAll(async () => { await db.disconnect(); });
    beforeEach(async () => { await db.clear(); });
    afterEach(() => { jest.restoreAllMocks(); });
});

基于属性的测试

基于属性的测试生成随机输入以测试不变量。fc.assert 多次运行属性。捕获基于示例的测试遗漏的边缘情况。

typescript
import { fc } from 'fast-check';
describe('sort', () => {
    it('preserves length', () => {
        fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
            return [...arr].sort().length === arr.length;
        }));
    });
});
30

常见陷阱

any vs unknown

any 禁用类型检查,隐藏 bug。unknown 是类型安全的:使用前必须收窄。不可信来源 (API, JSON.parse) 使用 unknown。

typescript
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data)
        return (data as any).foo;
}

多余属性检查

TypeScript 仅检查直接赋值的对象字面量的多余属性。通过变量,检查被跳过。使用 zod 进行更严格的运行时验证。

typescript
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true };  // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data;  // OK

枚举 vs 联合

枚举创建带反向映射的运行时对象。联合类型零运行时且可 tree-shake。新代码优先使用联合类型。

typescript
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }

结构化类型

TypeScript 使用结构化类型:形状匹配则类型兼容。Admin 可赋值给 User。这可能导致逻辑 bug。品牌类型添加名义区分。

typescript
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin;  // OK (structural)

类型断言危险

类型断言 (as) 无运行时检查地覆盖 TypeScript。对外部数据使用运行时验证 (zod, io-ts)。safeParse 返回结果而不抛出。

typescript
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。