TypeScript 核心概念与类型系统

中等 🟡Js/Ts
6 个标签
预计阅读时间:129 分钟
TypeScript类型系统接口泛型类型守卫模块

TypeScript 核心概念与类型系统

TypeScript 是 JavaScript 的超集(Superset),为 JavaScript 添加了静态类型系统,提高了代码的可靠性和可维护性。它由微软在 2012 年推出,如今已成为中大型前端项目的事实标准。

"超集"是什么意思? 意思是任何合法的 JavaScript 代码都是合法的 TypeScript 代码。你可以把 `.js` 直接改名为 `.ts`,它依然能跑。TypeScript 在此基础上叠加了一层"类型标注",这些标注只在开发和编译阶段存在,编译后会被完全擦除,产物就是普通的 JavaScript。

可以用一个类比理解:JavaScript 像是不带说明书的乐高零件,你得凭记忆知道每块怎么拼;TypeScript 则是给每块零件都贴上了标签和拼装说明,一旦你拼错,说明书(编译器)会立刻提醒你,而不是等成品散架了才发现。

为什么要用 TypeScript

静态类型带来的收益是实打实的。微软和一些大型开源项目的统计显示,接入 TypeScript 后,约 15% 的常见 bug(类型相关、拼写错误、空值访问等)能在编译期被直接拦截,根本进不到运行时。

| 维度 | 纯 JavaScript | TypeScript |

| --- | --- | --- |

| 错误发现时机 | 运行时(用户触发) | 编译期(写代码时) |

| IDE 智能提示 | 有限、靠猜 | 精准、可跳转 |

| 重构安全性 | 靠全局搜索、易漏 | 类型驱动、改错即报 |

| 自文档化 | 需额外注释 | 类型即文档 |

| 大型协作 | 接口靠口头约定 | 接口由类型强制 |

| 学习/接入成本 | 无 | 有一定门槛 |

代价是需要写类型标注、配置编译工具、学习类型系统。但对于超过几千行、需要多人协作或长期维护的项目,这个投入回报比极高。

类型系统

TypeScript 的类型系统是"结构化类型系统"(Structural Typing),也叫"鸭子类型"——只要结构长得一样,就认为是同一类型,不关心名字。

基本类型:

原始类型:`string`、`number`、`boolean`、`null`、`undefined`、`symbol`、`bigint`
对象类型:`object`、数组、函数
特殊类型:`any`、`unknown`、`never`、`void`
联合类型:`A | B`(是 A 或 B)
交叉类型:`A & B`(同时是 A 和 B)

类型推断:

TypeScript 拥有强大的类型推断能力,可以根据变量的初始值、函数的返回值、变量的使用方式等自动推断类型。例如 `let x = 10` 会自动推断 `x` 为 `number`;`const greeting = (name: string) => \`Hello, \${name}\`` 会推断返回类型为 `string`。合理利用类型推断可以减少类型注解的工作量,同时保持类型安全。
基于初始化值
基于上下文(Contextual Typing)

类型断言:

告诉 TypeScript 变量的具体类型("我比编译器更清楚")
使用 `as` 关键字
使用尖括号语法(在 JSX 中不推荐,会与标签冲突)

代码示例

基本类型示例

typescriptCode
// 原始类型
let name: string = 'Alice';
let age: number = 25;
let isStudent: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let unique: symbol = Symbol('unique');
let bigNumber: bigint = 100n;

// 对象类型
let person: object = { name: 'Alice', age: 25 };
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
let fn: Function = () => {};

// 联合类型
let value: string | number = 'hello';
value = 42;

// 交叉类型
type Person = { name: string };
type Employee = { id: number };
type PersonEmployee = Person & Employee;

const personEmployee: PersonEmployee = {
  name: 'Alice',
  id: 1
};

字面量类型、元组与枚举

除了宽泛的原始类型,TypeScript 还能把"具体的值"当作类型,这在建模有限状态时极其有用。

typescriptCode
// 字面量类型:值本身即类型
let direction: 'up' | 'down' | 'left' | 'right';
direction = 'up';    // OK
// direction = 'top'; // 错误:不在允许集合内

// 元组:固定长度、每个位置类型明确的数组
let pair: [string, number] = ['age', 25];
let rgb: [number, number, number] = [255, 128, 0];
// React 的 useState 返回值就是元组:[state, setState]

// 枚举:一组命名常量
enum Status {
  Pending,   // 0
  Active,    // 1
  Closed,    // 2
}
let s: Status = Status.Active;

// 字符串枚举(更利于调试,值有意义)
enum Role {
  Admin = 'ADMIN',
  User = 'USER',
  Guest = 'GUEST',
}

// 现代实践:常量对象 + as const 常被用来替代枚举
const LogLevel = {
  Debug: 'debug',
  Info: 'info',
  Error: 'error',
} as const;
type LogLevel = typeof LogLevel[keyof typeof LogLevel];
// 'debug' | 'info' | 'error'

any、unknown、never、void 辨析

这四个特殊类型是初学者最容易混淆的,也是面试高频考点。

typescriptCode
// any:放弃类型检查,"逃生舱",应尽量避免
let a: any = 4;
a.foo.bar;        // 不报错,但运行时可能崩溃
a = 'string';     // 随意赋值

// unknown:类型安全版的 any,用前必须收窄
let u: unknown = getValue();
// u.toFixed(2);   // 错误:不能直接使用
if (typeof u === 'number') {
  u.toFixed(2);    // OK:收窄后才能用
}

// void:表示函数没有返回值
function logMessage(msg: string): void {
  console.log(msg);
}

// never:永远不会有值(抛异常或死循环)
function throwError(msg: string): never {
  throw new Error(msg);
}
// never 也用于穷尽性检查(见下文类型守卫)

| 类型 | 含义 | 能否赋给它 | 能否从它取值 | 建议 |

| --- | --- | --- | --- | --- |

| any | 关闭检查 | 任何值 | 任意使用 | 尽量不用 |

| unknown | 未知但安全 | 任何值 | 收窄后才能用 | 优先用它替代 any |

| void | 无返回值 | undefined | 无意义 | 标注无返回函数 |

| never | 不可能有值 | 无 | 无 | 穷尽检查、异常函数 |

类型推断示例

typescriptCode
// 基于初始化值
let userName = 'Alice';  // 推断为 string
let count = 25;          // 推断为 number
let isActive = true;     // 推断为 boolean

// const 会推断为更窄的字面量类型
const literal = 'hello'; // 推断为 'hello',而非 string

// 基于上下文(回调参数类型自动推断)
const numbers = [1, 2, 3];
numbers.forEach(num => {
  console.log(num.toFixed(2)); // num 被推断为 number
});

// 函数返回值推断
function add(a: number, b: number) {
  return a + b;   // 返回类型推断为 number
}

// 显式类型注解(先声明后赋值时需要)
let total: number;
total = 10;

类型断言示例

typescriptCode
// 使用 as 关键字
let value: unknown = 'hello';
let length: number = (value as string).length;

// 使用尖括号语法(在 JSX 中不推荐)
let length2: number = (<string>value).length;

// 非空断言:告诉编译器"这里一定不是 null/undefined"
function printName(name: string | null) {
  console.log(name!.toUpperCase()); // ! 断言 name 不为 null
}

// DOM 元素类型断言
const button = document.querySelector('button') as HTMLButtonElement;
button.addEventListener('click', () => {
  console.log('Button clicked');
});

// const 断言:把整个对象/数组冻结为最窄的只读字面量类型
const config = {
  endpoint: '/api',
  retries: 3,
} as const;
// config 的类型为 { readonly endpoint: '/api'; readonly retries: 3 }

断言的风险: 断言是"绕过"检查,不是"转换"。如果你断言错了,编译器信你,运行时就会崩。所以断言要慎用,优先用类型守卫收窄。

接口和类型别名

接口(interface):

定义对象的结构
可以被 `extends` 扩展
可以被类 `implements` 实现
同名接口会自动合并(声明合并)
适合定义对象类型和公共 API

类型别名(type):

为任意类型创建别名
可以表示联合、交叉、元组、原始类型等任何类型
通过交叉 `&` 组合,不能像接口那样重复声明合并
适合定义联合类型、交叉类型、复杂类型

| 特性 | interface | type |

| --- | --- | --- |

| 描述对象结构 | 擅长 | 可以 |

| 联合/交叉类型 | 不能直接表达联合 | 擅长 |

| 扩展方式 | extends | & 交叉 |

| 声明合并 | 支持 | 不支持 |

| 表达原始类型/元组 | 不能 | 可以 |

| 性能(大型项目) | 略优(缓存友好) | 复杂交叉略慢 |

经验法则:描述对象/类的公共契约用 interface,需要联合类型或复杂类型运算用 type。 两者能力大幅重叠,团队内保持一致即可。

代码示例

接口示例

typescriptCode
// 基本接口
interface Person {
  name: string;
  age: number;
}

const person: Person = {
  name: 'Alice',
  age: 25
};

// 可选属性
interface Product {
  id: number;
  name: string;
  price?: number; // 可选属性
}

const product: Product = {
  id: 1,
  name: 'Laptop'
};

// 只读属性
interface Point {
  readonly x: number;
  readonly y: number;
}

const point: Point = { x: 10, y: 20 };
// point.x = 15; // 错误:只读属性不能修改

// 索引签名:允许任意字符串键
interface StringMap {
  [key: string]: string;
}
const headers: StringMap = { 'Content-Type': 'application/json' };

// 函数类型接口
interface SearchFn {
  (source: string, keyword: string): boolean;
}
const search: SearchFn = (src, kw) => src.includes(kw);

// 接口扩展
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const dog: Dog = {
  name: 'Buddy',
  breed: 'Golden Retriever'
};

// 接口实现
interface Clock {
  currentTime: Date;
  setTime(d: Date): void;
}

class DigitalClock implements Clock {
  currentTime: Date = new Date();

  setTime(d: Date) {
    this.currentTime = d;
  }
}

// 声明合并:同名接口自动合并(常用于扩展第三方类型)
interface Window {
  myGlobalConfig: { debug: boolean };
}

类型别名示例

typescriptCode
// 基本类型别名
type Name = string;
type Age = number;
type Person = {
  name: Name;
  age: Age;
};

// 联合类型别名
type Status = 'pending' | 'success' | 'error';
type ID = string | number;

// 可辨识联合(Discriminated Union):最强大的建模工具
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

// 函数类型别名
type Callback = (error: Error | null, data?: any) => void;

function fetchData(callback: Callback) {
  // ...
}

// 泛型类型别名
type Container<T> = { value: T };
type NumberContainer = Container<number>;

// 条件类型别名
type NonNullableType<T> = T extends null | undefined ? never : T;
type Result = NonNullableType<string | null>; // string

泛型

泛型(Generics)的概念:

允许在定义函数、接口或类时使用类型参数(类型的"占位符")
提高代码的复用性,避免为每种类型重复写逻辑
保持类型安全,比用 `any` 强得多

打个比方:泛型就像函数的参数,只不过传的不是值,而是类型。`identity(arg: T)` 里的 `T` 就像一个"类型变量",调用时才确定它是什么。

泛型的使用场景:

泛型函数
泛型接口
泛型类
泛型约束(`extends`)
泛型默认值

泛型工具类型(内置):

`Partial`:将 T 的所有属性变为可选,通过映射类型给每个属性加 `?`。常用于"更新部分字段"的场景,如更新用户信息时只传要改的字段。
`Required`:与 Partial 相反,把所有可选属性变为必需(移除 `?`)。用于确保对象字段齐全。
`Readonly`:给所有属性加 `readonly`,防止被意外修改。常用于配置对象、常量。
`Pick`:从 T 中挑选指定属性 K 组成新类型,如 `Pick`。用于只需要部分字段时。
`Omit`:从 T 中排除指定属性 K,如 `Omit`。用于去掉敏感字段。
`Record`:构造键为 K、值为 T 的对象类型,如 `Record`。用于字典、映射、缓存。

代码示例

泛型函数示例

typescriptCode
// 基本泛型函数
function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42);
const str = identity('hello'); // 类型推断为 string

// 多个类型参数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const pairResult = pair('hello', 42); // [string, number]

// 泛型约束:限制类型参数必须满足某种结构
interface Lengthwise {
  length: number;
}

function getLength<T extends Lengthwise>(arg: T): number {
  return arg.length;
}

getLength('hello');   // 5
getLength([1, 2, 3]); // 3
// getLength(42);     // 错误:number 没有 length 属性

// 用 keyof 约束键,实现类型安全的属性访问
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { id: 1, name: 'Alice' };
const id = getProp(user, 'id');     // 类型为 number
const name = getProp(user, 'name'); // 类型为 string
// getProp(user, 'email');          // 错误:'email' 不存在

// 泛型默认值
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

泛型接口示例

typescriptCode
// 泛型接口
interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };

// 泛型接口作为函数类型
interface Comparator<T> {
  (a: T, b: T): number;
}

const numberComparator: Comparator<number> = (a, b) => a - b;
const stringComparator: Comparator<string> = (a, b) =>
  a.localeCompare(b);

// 泛型接口作为类类型
interface Repository<T> {
  findById(id: number): T | null;
  save(entity: T): void;
}

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

class UserRepository implements Repository<User> {
  private users: User[] = [];

  findById(id: number): User | null {
    return this.users.find(u => u.id === id) || null;
  }

  save(user: User): void {
    this.users.push(user);
  }
}

// 泛型接口描述通用 API 响应
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;

泛型类示例

typescriptCode
// 泛型类
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 numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.pop()); // 'world'

// 泛型 + 约束的类:类型安全的键值缓存
class Cache<K extends string | number, V> {
  private store = new Map<K, V>();

  set(key: K, value: V): void {
    this.store.set(key, value);
  }

  get(key: K): V | undefined {
    return this.store.get(key);
  }
}
const cache = new Cache<string, User>();
cache.set('u1', { id: 1, name: 'Alice' });

泛型工具类型示例

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

// Partial<T>:将所有属性变为可选
type PartialUser = Partial<User>;
const partialUser: PartialUser = {
  name: 'Alice'
};

// 典型用途:更新函数只需传部分字段
function updateUser(id: number, changes: Partial<User>) {
  // 只更新传入的字段
}
updateUser(1, { age: 26 });

// Required<T>:将所有属性变为必需
type RequiredUser = Required<Partial<User>>;
const requiredUser: RequiredUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  age: 25
};

// Readonly<T>:将所有属性变为只读
type ReadonlyUser = Readonly<User>;
const readonlyUser: ReadonlyUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  age: 25
};
// readonlyUser.name = 'Bob'; // 错误

// Pick<T, K>:拣选属性
type UserBasicInfo = Pick<User, 'id' | 'name'>;
const basicInfo: UserBasicInfo = {
  id: 1,
  name: 'Alice'
};

// Omit<T, K>:排除属性(如返回给前端时去掉敏感字段)
type PublicUser = Omit<User, 'email'>;
const publicUser: PublicUser = {
  id: 1,
  name: 'Alice',
  age: 25
};

// Record<K, T>:创建键值对类型
type UserRecord = Record<number, User>;
const users: UserRecord = {
  1: { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 },
  2: { id: 2, name: 'Bob', email: 'bob@example.com', age: 30 }
};

// 组合使用:常见的表单/DTO 建模
type CreateUserDTO = Omit<User, 'id'>;          // 创建时无 id
type UpdateUserDTO = Partial<Omit<User, 'id'>>; // 更新时字段全可选

类型守卫与类型收窄

类型收窄(Narrowing)是 TypeScript 日常使用中最重要的技能之一:当一个变量是联合类型时,如何在特定代码块里"缩小"到具体类型。

typescriptCode
// 1. typeof 守卫(原始类型)
function format(value: string | number): string {
  if (typeof value === 'number') {
    return value.toFixed(2); // 这里 value 是 number
  }
  return value.toUpperCase(); // 这里 value 是 string
}

// 2. instanceof 守卫(类实例)
function logError(err: Error | string) {
  if (err instanceof Error) {
    console.log(err.stack); // err 是 Error
  } else {
    console.log(err);       // err 是 string
  }
}

// 3. in 守卫(判断属性是否存在)
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly();  // Bird
  }
}

// 4. 自定义类型守卫(is 关键字)
interface Cat { meow: () => void }
interface Dog { bark: () => void }
function isCat(pet: Cat | Dog): pet is Cat {
  return 'meow' in pet;
}
function speak(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow();
  } else {
    pet.bark();
  }
}

// 5. never 实现穷尽性检查
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:
      // 若将来新增 shape 却忘了处理,这里会编译报错
      const exhaustive: never = shape;
      return exhaustive;
  }
}

高级类型

条件类型:

基于条件选择类型:`T extends U ? X : Y`
结合 `infer` 关键字推断/提取类型

映射类型:

基于现有类型创建新类型
遍历现有类型的属性并逐一转换

模板字面量类型:

使用字符串模板语法在类型层面拼接字符串
适合创建动态、精确的字符串类型

代码示例

条件类型示例

typescriptCode
// 基本条件类型
type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

// 条件类型与联合类型(分布式)
type NonNullableType<T> = T extends null | undefined ? never : T;
type Test3 = NonNullableType<string | null>; // string

// infer 关键字:提取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function foo(): string {
  return 'hello';
}
type FooReturn = MyReturnType<typeof foo>; // string

// 提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type NumberElement = ArrayElement<number[]>; // number

// 提取 Promise 的值类型
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
type StringValue = PromiseValue<Promise<string>>; // string

映射类型示例

typescriptCode
// 基本映射类型
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

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

type ReadonlyUser = MyReadonly<User>;
type PartialUser = MyPartial<User>;

// 映射类型 + 键重映射:生成 getter
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type PersonGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

// 映射类型 + 模板字面量类型:生成事件处理器
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => void;
};

interface Events {
  click: MouseEvent;
  change: Event;
}

type EventHandlersType = EventHandlers<Events>;
// {
//   onClick?: (event: MouseEvent) => void;
//   onChange?: (event: Event) => void;
// }

模板字面量类型示例

typescriptCode
// 基本模板字面量类型
type Greeting = `Hello, ${string}!`;
const greeting: Greeting = 'Hello, World!';

// 模板字面量类型 + 联合类型(笛卡尔积)
type Color = 'red' | 'green' | 'blue';
type Shade = 'light' | 'dark';
type ColorShade = `${Shade}-${Color}`;

const color1: ColorShade = 'light-red';
const color2: ColorShade = 'dark-blue';

// 事件名类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'

// 实际应用:CSS 自定义属性
type CSSVar<T extends string> = `--${T}`;
type PrimaryColorVar = CSSVar<'primary-color'>; // '--primary-color'

模块系统

模块的概念:

代码的独立单元,每个文件就是一个模块
通过导出(export)和导入(import)共享功能
避免全局命名冲突

导出方式: 命名导出、默认导出、重导出。

导入方式: 命名导入、默认导入、命名空间导入、动态导入。

代码示例

模块导出示例

typescriptCode
// utils.ts
// 命名导出
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

// 默认导出
export default function multiply(a: number, b: number): number {
  return a * b;
}

// 类型导出(推荐用 export type,编译时可被完全擦除)
export interface User {
  id: number;
  name: string;
}
export type ID = string | number;

// 重导出(聚合多个模块,常用于 index.ts)
export { add, subtract } from './math';
export * from './constants';
export type { Config } from './config';

模块导入示例

typescriptCode
// main.ts
// 命名导入
import { add, subtract } from './utils';

// 仅导入类型(type-only import,避免副作用、优化打包)
import type { User } from './utils';

// 默认导入
import multiply from './utils';

// 命名空间导入
import * as utils from './utils';
utils.add(1, 2);

// 混合导入
import multiply2, { add as sum } from './utils';

// 动态导入(按需加载,实现代码分割)
async function loadModule() {
  const utils = await import('./utils');
  console.log(utils.add(1, 2));
}

tsconfig 与 strict 模式

`tsconfig.json` 控制编译行为。其中最重要的是 `strict` 系列开关,强烈建议新项目一律开启。

jsonCode
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,              // 一键开启所有严格检查
    "noImplicitAny": true,       // 禁止隐式 any
    "strictNullChecks": true,    // null/undefined 必须显式处理
    "noUnusedLocals": true,      // 禁止未使用的变量
    "noImplicitReturns": true,   // 函数所有分支都要有返回
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

其中 `strictNullChecks` 收益最大:它把 `null` 和 `undefined` 从"任何类型的合法值"中剥离出来,逼你显式处理空值,能消灭绝大部分"Cannot read property of undefined"这类线上崩溃。

真实案例:用类型驱动重构 API 层

某团队的前端项目里,后端接口返回的数据结构散落在各处,字段拼写错误频发。引入 TypeScript 后,他们做了如下改造:

typescriptCode
// 1. 用一份类型定义描述接口契约
interface UserDTO {
  id: number;
  userName: string;   // 注意:后端是 userName 而非 username
  createdAt: string;
}

// 2. 封装类型安全的请求函数
async function apiGet<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

// 3. 使用时自动获得字段提示与检查
const user = await apiGet<UserDTO>('/api/users/1');
console.log(user.userName);   // IDE 自动提示,拼错立刻报红
// console.log(user.username); // 编译错误,拼写错误被拦截

改造后,该模块因字段拼写、结构不一致导致的 bug 从每月十余个降到接近零,新人上手速度也明显提升——因为类型定义本身就是最新的接口文档。

常见坑

滥用 any:一旦用 any,该处的类型安全全部失效,且会"传染"。优先用 unknown。
类型断言当转换用:`as` 只是欺骗编译器,断言错了运行时照样崩。
忽视 strictNullChecks:不开的话 null 崩溃防不住,强烈建议开启。
interface 与 type 混用无规范:团队应约定统一风格。
枚举的编译产物:普通 enum 会生成运行时代码,追求极致体积可用 const enum 或常量对象。
忘记 import type:类型导入未标注 type 可能引入不必要的运行时依赖。
过度类型标注:能推断的就别手写,冗余标注反而增加维护成本。

最佳实践

新项目一律开启 `strict` 模式
优先使用类型推断,只在必要处显式标注
描述对象契约用 interface,联合/复杂类型用 type
合理使用泛型提高复用性,但避免过度抽象
用类型守卫收窄,而非到处 `as` 断言
避免 any,用 unknown + 收窄替代
用可辨识联合 + never 穷尽检查建模有限状态
类型导入使用 `import type`
把类型定义当文档维护,与实现同步更新

基础类型深入

前面的章节介绍了原始类型的写法,本节把最容易出坑的几个基础类型讲透:字面量类型、元组、枚举,以及 `any`/`unknown`/`never`/`void` 这四个特殊类型的区别。这些内容看似基础,却是 90% 类型 bug 的源头。

字面量类型与联合收窄

概念:字面量类型(Literal Type)把"具体的值"提升为类型。`'up'` 不再只是一个字符串值,而是一个只能取 `'up'` 这一个值的类型。它通常配合联合类型使用,用来建模"有限取值集合"。

原理:TypeScript 有两种字面量推断模式——宽化(Widening)和不宽化。用 `let` 声明时会被宽化成基础类型(`let x = 'up'` 推断为 `string`),用 `const` 声明时保持字面量类型(`const x = 'up'` 推断为 `'up'`)。这个差异是很多"为什么我的类型变宽了"问题的根源。

typescriptCode
// let 宽化:类型变成 string
let a = 'up';            // 推断类型: string
// const 不宽化:类型是字面量 'up'
const b = 'up';          // 推断类型: 'up'

// 对象属性默认宽化
const config = { method: 'GET' };   // method 推断为 string
// 用 as const 冻结为字面量
const config2 = { method: 'GET' } as const; // method 为 'GET',且整个对象 readonly

// 字面量联合:建模有限状态
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
function request(url: string, method: HttpMethod) {
  // method 只能是这 5 个值之一
}
request('/api', 'GET');   // OK
// request('/api', 'get'); // Error: 'get' 不能赋给 HttpMethod

// 数字字面量联合
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
// 布尔字面量(较少单独用)
type Truthy = true;

案例:假设一个按钮组件有 3 种尺寸、4 种变体。用字面量联合建模,调用方拼错字符串会在编译期直接报错,而不是等到运行时样式错乱。

typescriptCode
type Size = 'small' | 'medium' | 'large';
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';

interface ButtonProps {
  size: Size;
  variant: Variant;
  disabled?: boolean;
}

// 传错值立即报错,IDE 还会自动补全 3 个 size 选项
const props: ButtonProps = { size: 'medium', variant: 'primary' };

常见坑:把可变对象传给需要字面量的参数时会报错,因为对象属性被宽化了。解决办法是加 `as const`,或显式标注类型。

typescriptCode
const opts = { method: 'GET' };
// request('/api', opts.method); // Error: string 不能赋给 HttpMethod
const opts2 = { method: 'GET' } as const;
request('/api', opts2.method);   // OK,method 为 'GET'

元组(Tuple)

概念:元组是"长度固定、每个位置类型确定"的数组。普通数组 `number[]` 表示任意个 number,元组 `[number, string]` 表示恰好第 0 位是 number、第 1 位是 string。

原理:元组在运行时就是普通 JS 数组,类型信息只在编译期存在。TypeScript 4.0 起支持具名元组元素和可变元组(Variadic Tuple Types),大幅增强了表达能力。

typescriptCode
// 基础元组
let pair: [string, number] = ['age', 25];
// pair = [25, 'age']; // Error: 位置类型不匹配

// 具名元组(4.0+):只是文档提示,不影响运行时
type Point = [x: number, y: number];
const p: Point = [10, 20];

// 可选元素
type Range = [start: number, end?: number];
const r1: Range = [0];
const r2: Range = [0, 100];

// 剩余元素
type StringThenNumbers = [string, ...number[]];
const s: StringThenNumbers = ['sum', 1, 2, 3, 4];

// 只读元组
type RO = readonly [number, number];
const ro: RO = [1, 2];
// ro[0] = 5; // Error: 只读

// 元组常见用法:模拟多返回值(类似 React useState)
function useToggle(init: boolean): [boolean, () => void] {
  let state = init;
  const toggle = () => { state = !state; };
  return [state, toggle];
}
const [on, toggleOn] = useToggle(false);

可变元组类型(Variadic Tuple) 让我们能对元组做泛型级别的拼接,是实现类型安全 `curry`、`concat` 的基础:

typescriptCode
// 把两个元组拼起来
type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U];
type R = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

// 提取首元素与剩余
type Head<T extends readonly unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type H = Head<[1, 2, 3]>; // 1
type T2 = Tail<[1, 2, 3]>; // [2, 3]

元组 vs 数组对比

| 维度 | 数组 number[] | 元组 [number, string] |

| --- | --- | --- |

| 长度 | 任意 | 固定(除非有 rest) |

| 各位置类型 | 统一 | 可不同 |

| 越界访问 | 类型为元素类型 | 报错或 undefined |

| 典型场景 | 列表、集合 | 多返回值、坐标、键值对 |

| 解构提示 | 每项同类型 | 每位精确类型 |

常见坑:元组的 `push` 不会报错但会破坏长度约束——TypeScript 无法在类型层面阻止 `tuple.push()`。若要严格,用 `readonly` 元组。

枚举(Enum)深入

概念:枚举给一组命名常量赋予统一类型。TypeScript 有三种枚举:数字枚举、字符串枚举、`const enum`。它们的编译产物差异巨大,是面试高频考点。

原理与编译产物:普通枚举会编译成一个真实的对象(IIFE),既能正向映射也能反向映射;`const enum` 在编译期被内联,不生成任何运行时对象。

typescriptCode
// 数字枚举:默认从 0 递增
enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right,  // 3
}
// 可手动赋起始值,后续递增
enum StatusCode {
  OK = 200,
  NotFound = 404,
  ServerError = 500,
}

// 字符串枚举:无自增,每个都要赋值
enum LogLevel {
  Debug = 'DEBUG',
  Info = 'INFO',
  Warn = 'WARN',
  Error = 'ERROR',
}

数字枚举的编译产物(关键:反向映射):

typescriptCode
// 上面的 Direction 编译成大致如下 JS:
var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
  Direction[Direction["Left"] = 2] = "Left";
  Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
// 结果:Direction.Up === 0,且 Direction[0] === 'Up'(反向映射)
// 字符串枚举只有正向映射,没有反向映射

`const enum` 的内联:

typescriptCode
const enum Color { Red, Green, Blue }
const c = Color.Green;
// 编译后直接内联为:const c = 1;(不生成 Color 对象)
// 优点:零运行时开销;缺点:无法反向映射、跨模块/isolatedModules 下有坑

枚举 vs 联合字面量对比(现代实践更推荐后者):

| 维度 | enum | as const 对象 / 字面量联合 |

| --- | --- | --- |

| 运行时产物 | 有对象(const enum 除外) | as const 有对象,纯联合无 |

| 反向映射 | 数字枚举支持 | 不支持 |

| Tree-shaking | 差(普通 enum) | 好 |

| 类型收窄 | 支持 | 支持且更直观 |

| 与 JS 生态兼容 | 一般 | 好 |

| 推荐度 | 逐渐降低 | 逐渐升高 |

替代枚举的现代写法:

typescriptCode
// 用 as const 对象替代枚举,既有运行时值又有类型
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;
type Direction = typeof Direction[keyof typeof Direction]; // 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'

function move(dir: Direction) { /* ... */ }
move(Direction.Up);   // OK
move('UP');           // 也 OK,因为就是字符串字面量

常见坑

1.数字枚举允许把任意 number 赋进去(`let d: Direction = 99` 不报错),类型安全性弱于字符串枚举。
2.`const enum` 在 `isolatedModules`(Babel/esbuild/SWC 单文件编译)下无法正常内联,会报错或需要额外配置。
3.数字枚举的反向映射会污染对象的键,遍历 `Object.keys` 会拿到数字键和字符串键两份。

最佳实践:新项目优先用字符串枚举或 `as const` 对象;库作者若产物要被 Babel 处理,避免 `const enum`。

any / unknown / never / void 四大特殊类型

这四个类型最容易混淆,一张表讲清区别:

| 类型 | 含义 | 可赋值给它 | 它可赋值给 | 典型场景 |

| --- | --- | --- | --- | --- |

| any | 关闭类型检查 | 任何值 | 任何类型 | 迁移期、逃生舱(应尽量避免) |

| unknown | 类型安全的顶层类型 | 任何值 | 只能给 unknown/any | 接收未知输入,用前必须收窄 |

| never | 不可能有值的底层类型 | 无(除 never) | 任何类型 | 穷尽检查、抛异常函数返回 |

| void | 没有有意义的返回值 | undefined(及 null 非严格) | 只能给 void/any/unknown | 函数无返回、回调忽略返回值 |

any vs unknown:`any` 会传染并关闭检查,`unknown` 强制你先收窄再使用。

typescriptCode
function parseAny(json: string): any {
  return JSON.parse(json);
}
const a = parseAny('{}');
a.foo.bar.baz;   // 不报错,运行时可能崩——any 的危险

function parseUnknown(json: string): unknown {
  return JSON.parse(json);
}
const u = parseUnknown('{}');
// u.foo;        // Error: 对象类型为 unknown
if (typeof u === 'object' && u !== null && 'foo' in u) {
  // 收窄后才能访问
}

never 的用途:表示"永远不会发生"。抛异常或死循环的函数返回 `never`;穷尽检查时用它保证覆盖所有分支。

typescriptCode
// 返回 never 的函数
function fail(msg: string): never {
  throw new Error(msg);
}
function loop(): never {
  while (true) {}
}

// never 用于穷尽检查(exhaustive check)
type Shape =
  | { kind: 'circle'; r: number }
  | { kind: 'square'; size: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.r ** 2;
    case 'square': return s.size ** 2;
    default:
      // 若将来给 Shape 加了新成员却忘了处理,这里会编译报错
      const _exhaustive: never = s;
      return _exhaustive;
  }
}

void 的细节:`void` 类型的函数并非"不能返回值",而是"返回值不该被使用"。这让 `Array.forEach` 能接受返回任意值的回调。

typescriptCode
type VoidFn = () => void;
const fn: VoidFn = () => 42; // OK!返回值被忽略
// 但你不能利用它的返回值
const r = fn(); // r 的类型是 void

// 实用场景:forEach 回调可以直接写 arr.push(x) 而不报错
[1, 2, 3].forEach(x => console.log(x));

常见坑:把 `() => void` 的类型标注理解成"必须无返回",导致误判。实际它只是"调用方不看返回值"。

最佳实践:把 `any` 当最后手段,优先 `unknown`;用 `never` 做穷尽检查兜底;理解 `void` 的"忽略返回值"语义。

小结表

| 类型 | 一句话记忆 | 关键坑 |

| --- | --- | --- |

| 字面量类型 | 值即类型,配 as const 冻结 | let 宽化 |

| 元组 | 定长定序的数组 | push 破坏长度 |

| 数字枚举 | 有反向映射、运行时对象 | 可赋任意 number |

| 字符串枚举 | 只有正向映射 | 无自增 |

| const enum | 编译期内联零开销 | isolatedModules 冲突 |

| any | 关闭检查,会传染 | 尽量别用 |

| unknown | 安全版 any,用前收窄 | 不能直接访问 |

| never | 不可能的值 | 穷尽检查兜底 |

| void | 忽略返回值 | 不是"禁止返回" |

interface 与 type 深度对比

`interface` 和 `type` 是 TypeScript 里定义对象结构的两把利器,功能高度重叠却又各有专长。理解它们的差异,能让你在团队规范和库设计中做出正确取舍。

核心差异总览

| 维度 | interface | type |

| --- | --- | --- |

| 描述对象/类结构 | 擅长 | 可以 |

| 联合类型 | 不能 | 能(A \| B) |

| 交叉类型 | 用 extends 组合 | 用 & 组合 |

| 声明合并 | 支持(同名自动合并) | 不支持(重名报错) |

| 计算属性/映射类型 | 不支持 | 支持 |

| 元组、原始类型别名 | 不支持 | 支持 |

| 被 class implements | 支持 | 支持(非联合时) |

| 扩展性(库对外 API) | 更适合(可被外部合并扩展) | 相对封闭 |

| 错误信息可读性 | 通常更友好 | 复杂 type 可能很长 |

| 性能(大型项目编译) | 略优(可缓存) | 复杂交叉略慢 |

interface 能做而 type 做不到的:声明合并

概念:声明合并(Declaration Merging)指同名 `interface` 会自动合并成一个。这在为第三方库或全局对象扩展类型时极其有用。

typescriptCode
// 同名 interface 自动合并
interface User {
  name: string;
}
interface User {
  age: number;
}
// 合并后 User = { name: string; age: number }
const u: User = { name: 'Alice', age: 25 };

// 经典用法:扩展全局 Window
declare global {
  interface Window {
    myAppConfig: { version: string };
  }
}
window.myAppConfig = { version: '1.0.0' };

// 扩展第三方库的类型(如给 Express Request 加字段)
// declare module 'express' {
//   interface Request { userId?: string; }
// }

`type` 无法这样合并——同名 `type` 会直接报"重复标识符"。

type 能做而 interface 做不到的

typescriptCode
// 1. 联合类型
type Result = { ok: true; data: string } | { ok: false; error: string };

// 2. 原始类型/元组别名
type ID = string | number;
type Pair = [number, number];

// 3. 映射类型
type Optional<T> = { [K in keyof T]?: T[K] };

// 4. 模板字面量类型
type EventName = `on${string}`;

// 5. 条件类型
type NonNull<T> = T extends null | undefined ? never : T;

扩展方式对比

typescriptCode
// interface 用 extends(可多继承)
interface Animal { name: string; }
interface Pet extends Animal { owner: string; }
interface Dog extends Animal, Pet { breed: string; }

// type 用交叉 &
type Animal2 = { name: string };
type Pet2 = Animal2 & { owner: string };

// 互相扩展也可以
interface FromType extends Animal2 { extra: boolean; }
type FromInterface = Animal & { extra: boolean };

extends 与 & 的细微差别:`interface extends` 遇到同名不兼容属性会立即报错;`type &` 遇到冲突属性会得到 `never` 而不报错,可能埋坑。

typescriptCode
interface A { x: number }
// interface B extends A { x: string } // Error: 类型不兼容,立即暴露

type C = { x: number } & { x: string }; // x 变成 never,不报错但已损坏

索引签名、只读、可选、函数与构造签名

索引签名(Index Signature) 描述"键类型统一、数量不定"的对象:

typescriptCode
interface StringMap {
  [key: string]: string;
}
const headers: StringMap = { 'Content-Type': 'application/json' };

// 数字索引签名(数组式)
interface NumberList {
  [index: number]: string;
}

// 混合:具体属性 + 索引签名(具体属性类型必须兼容索引类型)
interface Dict {
  length: number;              // OK
  [key: string]: number;       // 所有键都必须是 number
}

只读与可选修饰符

typescriptCode
interface Config {
  readonly id: string;        // 只读,初始化后不可改
  name: string;               // 必填
  description?: string;       // 可选
  readonly tags?: string[];   // 只读且可选
}
const cfg: Config = { id: '1', name: 'app' };
// cfg.id = '2'; // Error: 只读属性
cfg.name = 'newName'; // OK

函数签名与构造签名:接口不仅能描述对象,还能描述可调用/可 new 的类型。

typescriptCode
// 函数类型接口
interface Adder {
  (a: number, b: number): number;   // 调用签名
  version: string;                   // 函数也能带属性
}
const add: Adder = Object.assign(
  (a: number, b: number) => a + b,
  { version: '1.0' }
);

// 构造签名(描述可被 new 的类型)
interface PointCtor {
  new (x: number, y: number): { x: number; y: number };
}
function createPoint(Ctor: PointCtor, x: number, y: number) {
  return new Ctor(x, y);
}

// 重载签名
interface StringOrNumber {
  (value: string): string;
  (value: number): number;
}

常见坑:索引签名会"淹没"具体属性的类型安全——一旦有 `[key: string]: number`,访问任意字符串键都被认为返回 number,即使那个键不存在。

最佳实践

1.定义对象/类的公共 API 用 `interface`(可被扩展、错误友好)。
2.需要联合、映射、条件、元组时用 `type`。
3.团队内保持一致比纠结哪个更好更重要。

小结表

| 需求 | 用 interface | 用 type |

| --- | --- | --- |

| 对象结构 | 首选 | 可以 |

| 联合类型 | 不行 | 必须 |

| 声明合并扩展库 | 必须 | 不行 |

| 映射/条件/模板类型 | 不行 | 必须 |

| class implements | 都行 | 都行 |

类(Class)深入

TypeScript 的类在 ES2015 class 基础上叠加了访问修饰符、参数属性、抽象类、`implements`、存取器等类型化能力。

访问修饰符

概念:`public`(默认,处处可访问)、`private`(仅类内部)、`protected`(类及子类内部)、`readonly`(初始化后只读)。

typescriptCode
class BankAccount {
  public owner: string;          // 默认 public,可省略
  private balance: number;       // 仅类内部可访问
  protected pin: string;         // 类及子类可访问
  readonly accountId: string;    // 只读

  constructor(owner: string, accountId: string, pin: string) {
    this.owner = owner;
    this.accountId = accountId;
    this.pin = pin;
    this.balance = 0;
  }

  deposit(amount: number) {
    this.balance += amount;      // OK:类内部
  }
  getBalance() { return this.balance; }
}

const acc = new BankAccount('Alice', 'A001', '1234');
acc.deposit(100);
// acc.balance;   // Error: private
// acc.accountId = 'x'; // Error: readonly

关键区别:TypeScript 的 `private` 是"编译期私有",编译后属性依然存在于对象上,运行时可被访问(如 `(acc as any).balance`)。而 ES 私有字段 `#` 是"运行时真私有"。

参数属性(Parameter Properties)

原理:在构造函数参数上加修饰符,TypeScript 会自动声明并赋值同名属性,省去样板代码。

typescriptCode
// 传统写法
class Point1 {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

// 参数属性写法(等价,代码量减少约 60%)
class Point2 {
  constructor(
    public x: number,
    public y: number,
    private readonly label: string = 'point'
  ) {}
  describe() { return `${this.label}(${this.x}, ${this.y})`; }
}
const pt = new Point2(1, 2);

抽象类与 implements

概念:`abstract` 类不能被实例化,只能被继承;抽象方法只有签名没有实现,强制子类实现。`implements` 让类承诺满足某个接口。

typescriptCode
abstract class Shape {
  abstract area(): number;        // 抽象方法,子类必须实现
  abstract name: string;          // 抽象属性
  describe(): string {            // 具体方法,可被继承
    return `${this.name} 面积为 ${this.area()}`;
  }
}

class Circle extends Shape {
  name = 'circle';
  constructor(private r: number) { super(); }
  area(): number { return Math.PI * this.r ** 2; }
}
// new Shape(); // Error: 抽象类不能实例化
const c = new Circle(5);
console.log(c.describe());

// implements:类必须满足接口结构
interface Serializable {
  serialize(): string;
}
class User implements Serializable {
  constructor(public name: string) {}
  serialize() { return JSON.stringify({ name: this.name }); }
}

abstract class vs interface 对比

| 维度 | abstract class | interface |

| --- | --- | --- |

| 可含实现 | 能(具体方法/字段) | 不能(仅签名) |

| 运行时产物 | 有(是真实的类) | 无(编译擦除) |

| 多继承 | 单继承 | 类可 implements 多个 |

| 构造函数 | 有 | 无 |

| 适用 | 共享实现的基类 | 纯契约约束 |

存取器、静态成员与私有字段 #

typescriptCode
class Temperature {
  private _celsius = 0;

  // getter/setter:像属性一样访问,内部可加校验
  get celsius(): number { return this._celsius; }
  set celsius(value: number) {
    if (value < -273.15) throw new Error('低于绝对零度');
    this._celsius = value;
  }
  get fahrenheit(): number { return this._celsius * 9 / 5 + 32; }

  // 静态成员:属于类本身,不属于实例
  static readonly ABSOLUTE_ZERO = -273.15;
  static fromFahrenheit(f: number): Temperature {
    const t = new Temperature();
    t.celsius = (f - 32) * 5 / 9;
    return t;
  }
}
const t = new Temperature();
t.celsius = 25;              // 调用 setter
console.log(t.fahrenheit);  // 调用 getter
console.log(Temperature.ABSOLUTE_ZERO);

// ES 私有字段 #:运行时真私有
class Counter {
  #count = 0;                // 外部完全无法访问,连 as any 都不行
  increment() { this.#count++; }
  get value() { return this.#count; }
}
const counter = new Counter();
counter.increment();
// counter.#count; // 语法错误:私有字段只能在类内部访问

private vs # 对比

| 维度 | TS private | ES #私有字段 |

| --- | --- | --- |

| 私有级别 | 编译期 | 运行时真私有 |

| 运行时可绕过 | 能(as any) | 不能 |

| 编译产物 | 普通属性 | WeakMap 或原生 #(视 target) |

| 子类访问 | protected 可 | 不可 |

| 命名冲突 | 按名字 | 独立命名空间 |

常见坑

1.TS `private` 只是编译期约束,敏感数据不要指望它保密,用 `#`。
2.抽象属性初始化时机——子类必须在声明或构造中赋值。
3.静态成员不能访问实例属性(`this` 指向类而非实例)。

最佳实践:需要运行时封装用 `#`;共享实现的层次结构用抽象类;纯契约用接口 + `implements`;用参数属性精简构造函数。

小结表

| 特性 | 用途 | 记忆点 |

| --- | --- | --- |

| public/private/protected | 访问控制 | private 仅编译期 |

| readonly | 初始化后只读 | 不影响引用内部可变 |

| 参数属性 | 精简构造赋值 | 修饰符写在参数上 |

| abstract | 强制子类实现 | 不可实例化 |

| implements | 类满足接口契约 | 只检查不继承实现 |

| #私有字段 | 运行时真私有 | 独立命名空间 |

| static | 类级成员 | 不访问实例 this |

泛型深入

泛型(Generics)是"类型的参数化"——把类型当参数传递,让函数、类、类型在保持类型安全的同时复用于多种类型。泛型是 TypeScript 类型系统的引擎。

泛型约束(Constraints)

概念:默认的泛型 `T` 可以是任何类型,你不能对它做任何假设。用 `extends` 给泛型加约束,就能安全地访问其成员。

typescriptCode
// 无约束:不能访问 .length,因为 T 可能是 number
// function longest<T>(a: T, b: T) { return a.length > b.length ? a : b; } // Error

// 加约束:T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]);        // OK,返回 number[]
longest('ab', 'abc');              // OK,返回 string
// longest(1, 2);                  // Error: number 没有 length

// keyof 约束:K 必须是 T 的键
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const person = { name: 'Alice', age: 25 };
const name = getProp(person, 'name');  // 类型精确为 string
const age = getProp(person, 'age');    // 类型精确为 number
// getProp(person, 'email');           // Error: 'email' 不是键

泛型默认参数

typescriptCode
// 泛型默认值:不传时用默认类型
interface ApiResponse<T = unknown> {
  code: number;
  data: T;
  message: string;
}
const r1: ApiResponse = { code: 0, data: null, message: 'ok' };      // T = unknown
const r2: ApiResponse<string[]> = { code: 0, data: [], message: '' };// T = string[]

// 多个泛型参数,后者可引用前者
class Container<T, U = T[]> {
  constructor(public item: T, public list: U) {}
}

泛型工具函数与泛型类

typescriptCode
// 泛型函数:类型随参数流动
function identity<T>(x: T): T { return x; }
const n = identity(42);       // n: number(推断)
const s = identity<string>('hi'); // 显式指定

// 泛型数组工具
function first<T>(arr: T[]): T | undefined { return arr[0]; }
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
  const result: U[] = [];
  for (let i = 0; i < arr.length; i++) result.push(fn(arr[i], i));
  return result;
}
const lengths = map(['a', 'bb', 'ccc'], s => s.length); // number[]

// 泛型类:类型安全的栈
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);
const top = numStack.pop(); // number | undefined

条件类型与 infer

概念:条件类型 `T extends U ? X : Y` 是"类型层面的三元表达式"。`infer` 关键字能在条件类型里"捕获"某个位置的类型并命名。

typescriptCode
// 基础条件类型
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hello'>;  // 'yes'
type B = IsString<42>;       // 'no'

// infer 捕获:提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type E1 = ElementType<number[]>;   // number
type E2 = ElementType<string[]>;   // string

// infer 捕获函数返回值(ReturnType 的原理)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => number>;        // number
type R2 = MyReturnType<(x: string) => void>; // void

// infer 捕获 Promise 解包
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type U1 = Unwrap<Promise<string>>; // string
type U2 = Unwrap<number>;          // number

// 嵌套 infer:提取第一个函数参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstArg<(name: string, age: number) => void>; // string

分布式条件类型(Distributive Conditional Types)

原理:当条件类型作用于"裸的泛型参数"且该参数是联合类型时,会自动"分发"到联合的每个成员上,分别计算再合并。这是很多工具类型的底层机制。

typescriptCode
// 分布式:联合被逐个处理
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[](分发了!)
// 计算过程:ToArray<string> | ToArray<number> = string[] | number[]

// 用 [T] 包裹可阻止分发
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNonDist<string | number>; // (string | number)[]

// 利用分发实现 Exclude
type MyExclude<T, U> = T extends U ? never : T;
type Without = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// 过程:('a' extends 'a' ? never : 'a') | ('b' ...) | ('c' ...)
//     = never | 'b' | 'c' = 'b' | 'c'

分发与否对比

| 写法 | 是否分发 | ToArray 结果 |

| --- | --- | --- |

| T extends any ? T[] : never | 分发 | A[] \| B[] |

| [T] extends [any] ? T[] : never | 不分发 | (A\|B)[] |

常见坑

1.`never` 作为分发对象时结果永远是 `never`(空联合),容易让工具类型"意外返回 never"。
2.忘记泛型参数必须是"裸类型"才分发——包在对象/元组里就不分发了。

最佳实践:需要逐成员处理联合时依赖分发;需要整体判断时用 `[T] extends [U]` 关闭分发。

小结表

| 特性 | 作用 | 关键词 |

| --- | --- | --- |

| extends 约束 | 限定泛型可取范围 | keyof、结构约束 |

| 默认参数 | 泛型省略时兜底 | T = unknown |

| 条件类型 | 类型层三元 | T extends U ? X : Y |

| infer | 捕获并命名类型 | 返回值、元素、Promise |

| 分布式条件 | 联合逐成员处理 | 裸类型才分发 |

类型收窄与类型守卫

联合类型让变量"可能是多种类型之一",而类型守卫(Type Guard)让 TypeScript 在特定代码块内把它收窄(Narrowing)到更具体的类型。这是写出健壮 TS 代码的核心技能。

typeof、instanceof、in、字面量收窄

typescriptCode
// typeof 收窄(用于原始类型)
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase(); // 此处 value 收窄为 string
  }
  return value.toFixed(2);      // 此处 value 收窄为 number
}

// instanceof 收窄(用于类实例)
class Dog { bark() {} }
class Cat { meow() {} }
function speak(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();  // 收窄为 Dog
  } else {
    animal.meow();  // 收窄为 Cat
  }
}

// in 收窄(按属性存在性)
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim();  // 收窄为 Fish
  } else {
    animal.fly();   // 收窄为 Bird
  }
}

// 字面量与真值收窄
function process(input: string | null) {
  if (input) {
    input.trim();   // 收窄为 string(排除了 null 和 '')
  }
}

自定义类型谓词 is

概念:返回类型写成 `arg is Type` 的函数是"类型谓词函数",调用后 TypeScript 会据其真假收窄类型。适合封装复杂的判断逻辑。

typescriptCode
interface Admin { role: 'admin'; permissions: string[]; }
interface Guest { role: 'guest'; }
type Account = Admin | Guest;

// 类型谓词:返回 account is Admin
function isAdmin(account: Account): account is Admin {
  return account.role === 'admin';
}

function handle(account: Account) {
  if (isAdmin(account)) {
    console.log(account.permissions); // 收窄为 Admin,可访问 permissions
  }
}

// 用于过滤数组并收窄元素类型
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}
const mixed = [1, undefined, 2, null, 3];
const cleaned = mixed.filter(isDefined); // number[](去掉了 undefined/null)

断言函数 asserts

概念:`asserts` 让函数在"不满足条件时抛错",之后的代码里 TypeScript 认为条件成立。分两种:`asserts cond` 和 `asserts x is Type`。

typescriptCode
// asserts 条件:断言后续代码里条件为真
function assert(condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new Error(msg ?? '断言失败');
}
function getLength(value: string | null): number {
  assert(value !== null, 'value 不能为空');
  return value.length; // 此后 value 收窄为 string
}

// asserts x is Type:断言 x 是某类型
function assertIsString(val: unknown): asserts val is string {
  if (typeof val !== 'string') throw new Error('不是字符串');
}
function useValue(input: unknown) {
  assertIsString(input);
  input.toUpperCase(); // input 收窄为 string
}

可辨识联合与 never 穷尽检查

概念:可辨识联合(Discriminated Union)是给联合的每个成员加一个共同的"判别字段"(如 `kind`/`type`),配合 `switch` 收窄,是建模状态机的黄金模式。

typescriptCode
// 建模异步请求的四种状态
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function render<T>(state: RequestState<T>): string {
  switch (state.status) {
    case 'idle':    return '空闲';
    case 'loading': return '加载中...';
    case 'success': return `数据:${JSON.stringify(state.data)}`; // 可访问 data
    case 'error':   return `错误:${state.error}`;                // 可访问 error
    default:
      // 穷尽检查:若漏了某个 status,这里编译报错
      const _exhaustive: never = state;
      return _exhaustive;
  }
}

为什么 never 穷尽检查重要:将来给 `RequestState` 加一个 `{ status: 'cancelled' }`,若忘了在 switch 里处理,`state` 在 default 分支就不再是 `never`,赋值给 `_exhaustive: never` 会立即编译报错,强制你补全逻辑。这是"用类型系统防止漏改"的经典手段。

收窄手段对比表

| 守卫方式 | 适用类型 | 示例 | 局限 |

| --- | --- | --- | --- |

| typeof | 原始类型 | typeof x === 'string' | 只有 8 种返回值 |

| instanceof | 类实例 | x instanceof Date | 需有构造函数 |

| in | 对象属性 | 'swim' in x | 靠属性名 |

| 真值/字面量 | 联合含 null 等 | if (x) | 注意 0、''、false |

| is 谓词 | 任意 | x is Admin | 逻辑自己保证正确 |

| asserts | 任意 | asserts x is T | 不满足则抛错 |

| 可辨识联合 | 带判别字段的联合 | switch(x.kind) | 需设计判别字段 |

常见坑

1.`typeof null === 'object'`,用 `typeof` 判断对象时要额外排除 null。
2.类型谓词函数的逻辑若写错(返回值与实际类型不符),TypeScript 会信任它,反而制造隐患。
3.真值收窄对 `0`、`''`、`NaN` 也会判为假,处理 `number | undefined` 时要用 `!== undefined` 而非 `if (x)`。

最佳实践:优先用可辨识联合建模状态;复杂判断封装成 `is` 谓词;用 `never` 兜底保证穷尽;避免用真值收窄处理可能为 0/'' 的值。

小结表

| 技术 | 一句话 | 记忆 |

| --- | --- | --- |

| typeof/instanceof/in | 内置收窄 | 分别管原始/实例/属性 |

| is 谓词 | 自定义收窄 | 返回 x is T |

| asserts | 断言收窄 | 不满足抛错 |

| 可辨识联合 | 状态建模 | 判别字段 + switch |

| never 穷尽 | 防漏改 | default 赋值 never |

内置工具类型全解与手写实现

TypeScript 内置了一批工具类型(Utility Types),能从已有类型派生出新类型。理解它们的源码实现,是掌握类型编程的最好途径。下面逐个给出用法、手写实现和逐行解释。

Partial / Required / Readonly

typescriptCode
// Partial<T>:把所有属性变可选
type MyPartial<T> = {
  [P in keyof T]?: T[P]; // 遍历 T 的每个键 P,加 ? 变可选
};

// Required<T>:把所有属性变必填(-? 去掉可选修饰符)
type MyRequired<T> = {
  [P in keyof T]-?: T[P]; // -? 移除可选标记
};

// Readonly<T>:把所有属性变只读
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]; // 每个键加 readonly
};

interface User { id: number; name: string; email?: string; }
type PartialUser = MyPartial<User>;   // { id?; name?; email? }
type RequiredUser = MyRequired<User>; // { id; name; email }(email 变必填)
type ReadonlyUser = MyReadonly<User>; // 全部只读

逐行解释:`[P in keyof T]` 是映射类型语法,`keyof T` 得到所有键的联合(如 `'id' | 'name' | 'email'`),`P in` 逐个遍历。`?`/`-?`/`readonly` 是修饰符,`+`/`-` 分别表示添加/移除。`T[P]` 是索引访问,取出该键对应的值类型。

Pick / Omit

typescriptCode
// Pick<T, K>:从 T 中挑选 K 指定的键
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]; // 只遍历 K(K 被约束为 T 的键子集)
};

// Omit<T, K>:从 T 中排除 K 指定的键
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
// 原理:先用 Exclude 从 keyof T 里去掉 K,再 Pick 剩下的

type UserPreview = MyPick<User, 'id' | 'name'>;      // { id; name }
type UserNoEmail = MyOmit<User, 'email'>;            // { id; name }

Omit 的两步拆解:`Exclude` 先算出"要保留的键",再交给 `Pick` 组装。注意 `Omit` 的 K 约束是 `keyof any`(即 `string | number | symbol`),所以传入不存在的键不会报错——这是 `Omit` 和 `Pick` 的一个差异。

Record

typescriptCode
// Record<K, V>:构造键为 K、值为 V 的对象类型
type MyRecord<K extends keyof any, V> = {
  [P in K]: V; // 遍历 K 的每个成员,值都设为 V
};

type PageInfo = { title: string };
type Pages = MyRecord<'home' | 'about' | 'contact', PageInfo>;
// { home: PageInfo; about: PageInfo; contact: PageInfo }

// 常用于字典/映射
type Scores = Record<string, number>;
const s: Scores = { math: 90, english: 85 };

Exclude / Extract / NonNullable

typescriptCode
// Exclude<T, U>:从联合 T 中去掉可赋给 U 的成员
type MyExclude<T, U> = T extends U ? never : T;

// Extract<T, U>:从联合 T 中保留可赋给 U 的成员
type MyExtract<T, U> = T extends U ? T : never;

// NonNullable<T>:去掉 null 和 undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;

type T1 = MyExclude<'a' | 'b' | 'c', 'a'>;        // 'b' | 'c'
type T2 = MyExtract<'a' | 'b' | 'c', 'a' | 'z'>;  // 'a'
type T3 = MyNonNullable<string | null | undefined>; // string

原理:这三者都利用了"分布式条件类型"——联合被逐成员判断,`never` 会从结果联合中消失,从而实现过滤效果。

ReturnType / Parameters / ConstructorParameters / InstanceType

typescriptCode
// ReturnType<T>:提取函数返回值类型
type MyReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never;

// Parameters<T>:提取函数参数类型(作为元组)
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// InstanceType<T>:提取构造函数返回的实例类型
type MyInstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : never;

function createUser(id: number, name: string) {
  return { id, name, createdAt: Date.now() };
}
type CreateUserReturn = MyReturnType<typeof createUser>;    // { id; name; createdAt }
type CreateUserArgs = MyParameters<typeof createUser>;       // [number, string]

class Widget { constructor(public w: number) {} }
type W = MyInstanceType<typeof Widget>;                      // Widget

Awaited(解包 Promise,4.5+)

typescriptCode
// 简化版 Awaited:递归解包嵌套 Promise
type MyAwaited<T> =
  T extends Promise<infer U>
    ? MyAwaited<U>   // 若解包后还是 Promise,继续递归
    : T;

type A1 = MyAwaited<Promise<string>>;               // string
type A2 = MyAwaited<Promise<Promise<number>>>;      // number(递归解到底)
type A3 = MyAwaited<boolean>;                        // boolean

// 实战:拿到 async 函数的真实返回值
async function fetchUser() { return { id: 1, name: 'Alice' }; }
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>; // { id; name }

内置工具类型速查表

| 工具类型 | 作用 | 实现核心 |

| --- | --- | --- |

| Partial | 全部可选 | [P in keyof T]? |

| Required | 全部必填 | [P in keyof T]-? |

| Readonly | 全部只读 | readonly [P in keyof T] |

| Pick | 挑选键 | [P in K] |

| Omit | 排除键 | Pick + Exclude |

| Record | 构造字典 | [P in K]: V |

| Exclude | 联合去成员 | T extends U ? never : T |

| Extract | 联合留成员 | T extends U ? T : never |

| NonNullable | 去 null/undefined | 条件排除 |

| ReturnType | 取返回值 | infer R |

| Parameters | 取参数元组 | infer P |

| InstanceType | 取实例类型 | new infer R |

| Awaited | 解包 Promise | 递归 infer |

常见坑

1.`Omit` 不校验键是否存在,拼错键名不报错;`Pick` 会报错。
2.对联合类型用 `Omit` 会"合并"成员,可能丢失可辨识性;此时需自己写分布式版本。
3.`Partial` 只作用一层,嵌套对象不会递归可选(需自定义 `DeepPartial`)。

高级类型与类型体操

映射类型、键重映射、模板字面量类型、递归类型组合起来,能在类型层面做相当复杂的"编程",俗称"类型体操"。

映射类型与键重映射(as)

概念:映射类型遍历一个类型的键生成新类型;TypeScript 4.1 引入的键重映射(Key Remapping)允许用 `as` 子句改写键名,甚至过滤键。

typescriptCode
// 基础映射 + 修饰符增删
type Mutable<T> = {
  -readonly [P in keyof T]: T[P]; // 移除 readonly
};

// 键重映射:给每个键加 get 前缀,生成 getter 类型
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// 键重映射过滤:用 never 剔除不想要的键
type RemoveKind<T> = {
  [P in keyof T as P extends 'kind' ? never : P]: T[P];
};
type WithoutKind = RemoveKind<{ kind: string; value: number }>; // { value: number }

// 只保留值为函数的键(提取方法)
type MethodsOnly<T> = {
  [P in keyof T as T[P] extends Function ? P : never]: T[P];
};

模板字面量类型

概念:模板字面量类型把字符串操作带到了类型层面,可以拼接、约束字符串格式,配合内置的 `Uppercase`/`Lowercase`/`Capitalize`/`Uncapitalize`。

typescriptCode
// 基础拼接
type Greeting = `Hello, ${string}!`;
const g1: Greeting = 'Hello, world!';  // OK
// const g2: Greeting = 'Hi';          // Error

// 联合分发式组合(笛卡尔积)
type Color = 'red' | 'blue';
type Shade = 'light' | 'dark';
type ColorShade = `${Shade}-${Color}`;
// 'light-red' | 'light-blue' | 'dark-red' | 'dark-blue'

// 内置字符串操作
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'

type Loud = Uppercase<'hello'>;       // 'HELLO'
type Quiet = Lowercase<'HELLO'>;      // 'hello'

// 结合 infer 解析字符串
type ExtractRouteParam<T extends string> =
  T extends `/${string}/:${infer Param}` ? Param : never;
type Param = ExtractRouteParam<'/users/:id'>; // 'id'

递归类型与类型体操实例

概念:类型可以引用自身,形成递归。配合条件类型和 `infer`,能实现深层变换。

typescriptCode
// DeepReadonly:递归只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>  // 值是对象则递归
    : T[P];
};
interface Nested { a: { b: { c: number } }; }
type FrozenNested = DeepReadonly<Nested>; // 所有层级只读

// DeepPartial:递归可选
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// 类型体操:数组长度(利用元组 length)
type Length<T extends readonly any[]> = T['length'];
type L = Length<[1, 2, 3]>; // 3

// 类型体操:字符串转联合
type StringToUnion<S extends string> =
  S extends `${infer First}${infer Rest}`
    ? First | StringToUnion<Rest>
    : never;
type Chars = StringToUnion<'abc'>; // 'a' | 'b' | 'c'

// 类型体操:反转元组
type Reverse<T extends any[]> =
  T extends [infer First, ...infer Rest]
    ? [...Reverse<Rest>, First]
    : [];
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]

递归深度限制:TypeScript 对类型递归有深度上限(默认约 50 层实例化深度,尾递归优化后可到约 1000 层)。写类型体操时超限会报 "Type instantiation is excessively deep and possibly infinite"。

高级类型能力对比

| 能力 | 语法 | 典型用途 |

| --- | --- | --- |

| 映射类型 | [P in keyof T] | 批量变换属性 |

| 键重映射 | as 新键名 | 改键名、过滤键 |

| 模板字面量类型 | `${A}-${B}` | 字符串格式约束 |

| 递归类型 | 类型引用自身 | DeepPartial、解析 |

| 条件+infer | extends ? : + infer | 提取、分解类型 |

常见坑

1.递归类型容易触发深度限制,尽量用尾递归形式(把累加器放元组)。
2.键重映射中 `Capitalize` 的 `string &` 是为了把 `P`(可能含 symbol)收窄成 string。
3.模板字面量联合会产生笛卡尔积,成员数量爆炸时编译会变慢。

最佳实践:类型体操适度使用,优先可读性;复杂类型加注释说明意图;能用内置工具类型就别重复造轮子。

小结表

| 技术 | 一句话 | 记忆 |

| --- | --- | --- |

| 映射类型 | 遍历键生成新类型 | [P in keyof T] |

| 键重映射 as | 改写/过滤键 | never 剔除 |

| 模板字面量类型 | 类型级字符串 | 笛卡尔积 |

| 递归类型 | 深层变换 | 注意深度限制 |

| infer | 类型解构 | 捕获子类型 |

模块、命名空间与声明文件

TypeScript 的模块系统建立在 ES Module 之上,并额外提供了命名空间、`declare`、`.d.ts` 声明文件和三斜线指令来描述"没有类型的 JS"。

ES 模块与 import type

typescriptCode
// 命名导出与导入
export interface User { id: number; name: string; }
export function createUser(name: string): User {
  return { id: Date.now(), name };
}
export const VERSION = '1.0.0';

// 默认导出
export default class ApiClient { /* ... */ }

// 仅导入类型(编译后完全擦除,不产生 import 语句)
import type { User } from './user';
// 混合导入,用 inline type 标记类型部分
import { createUser, type User as U } from './user';

为什么用 `import type`:明确标记"这个导入只用于类型",编译后会被彻底删除,避免因副作用导入引入不必要的运行时依赖,也能规避 `isolatedModules` 下的循环依赖问题。

命名空间(namespace)

概念:命名空间是 TypeScript 早期的模块化方案,把相关类型/值组织在一个全局对象下。现代项目基本被 ES 模块取代,但在编写全局类型声明时仍有用武之地。

typescriptCode
namespace Validation {
  export interface Validator {
    isValid(s: string): boolean;
  }
  export class EmailValidator implements Validator {
    isValid(s: string) { return /@/.test(s); }
  }
}
const v = new Validation.EmailValidator();

namespace vs module 对比

| 维度 | namespace | ES module |

| --- | --- | --- |

| 加载方式 | 全局合并 | 按文件按需 |

| Tree-shaking | 差 | 好 |

| 现代推荐 | 仅用于全局声明 | 首选 |

| 编译产物 | 命名空间对象 | import/export |

declare 与 .d.ts 声明文件

概念:`declare` 声明"某个东西在别处存在",只描述类型不生成实现。`.d.ts` 文件是纯类型声明文件,用来给 JS 库补类型,或声明全局变量。

typescriptCode
// global.d.ts —— 声明全局变量与模块
declare const __APP_VERSION__: string;         // 构建时注入的全局常量
declare function gtag(...args: any[]): void;    // 第三方脚本注入的全局函数

// 为没有类型的 JS 模块补声明
declare module 'legacy-lib' {
  export function doSomething(input: string): number;
  export const config: { debug: boolean };
}

// 为非 JS 资源声明模块(如 import 图片/样式)
declare module '*.svg' {
  const content: string;
  export default content;
}
declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

// 扩展全局接口
declare global {
  interface Window {
    dataLayer: unknown[];
  }
}
export {}; // 让本文件成为模块,declare global 才生效

三斜线指令与类型声明发布

typescriptCode
// 三斜线指令:引用其他声明文件或库类型(多见于 .d.ts)
/// <reference types="node" />
/// <reference path="./other.d.ts" />

类型声明发布方式

| 方式 | 说明 | 适用 |

| --- | --- | --- |

| 库内置 types | package.json 的 "types" 字段指向 .d.ts | 库作者自带类型 |

| @types/xxx | DefinitelyTyped 社区维护 | 第三方 JS 库 |

| 本地 d.ts | 项目内补声明 | 私有/无类型库 |

常见坑

1.`declare global` 必须在模块文件里(有 import/export),否则被当全局脚本,`global` 无效——常加 `export {}` 兜底。
2.`declare module '*.svg'` 这类声明要被 `include` 覆盖到才生效。
3.`import type` 与普通 import 混用时,删除代码后残留的类型导入可能报错,开启 `verbatimModuleSyntax` 更严格。

tsconfig 关键选项详解

`tsconfig.json` 控制编译器行为。理解关键选项能避免大量"为什么没报错/为什么报错"的困惑。

strict 家族

`strict: true` 是一个开关,一次性打开下面一整组严格检查:

| 选项 | 作用 | 关掉的风险 |

| --- | --- | --- |

| strictNullChecks | null/undefined 需显式处理 | 空值访问崩溃 |

| strictFunctionTypes | 函数参数逆变检查 | 回调类型不安全 |

| strictBindCallApply | bind/call/apply 参数检查 | 调用参数错配 |

| strictPropertyInitialization | 类属性必须初始化 | 未初始化属性 |

| noImplicitAny | 禁止隐式 any | 类型悄悄丢失 |

| noImplicitThis | 禁止隐式 any 的 this | this 指向错乱 |

| alwaysStrict | 输出 "use strict" | 非严格模式坑 |

| useUnknownInCatchVariables | catch 变量为 unknown | 错误类型误判 |

typescriptCode
// strictNullChecks 开启后的差异
function getLength(s: string | null) {
  // return s.length;      // Error: s 可能为 null
  return s?.length ?? 0;   // 正确处理
}

模块解析与路径

typescriptCode
// tsconfig.json 关键片段(示意)
// {
//   "compilerOptions": {
//     "target": "ES2020",            // 编译目标 JS 版本
//     "module": "ESNext",            // 输出模块格式
//     "moduleResolution": "Bundler", // 模块解析策略
//     "baseUrl": ".",
//     "paths": { "@/*": ["src/*"] }, // 路径别名
//     "lib": ["ES2020", "DOM"],      // 可用的内置类型库
//     "esModuleInterop": true,       // CJS/ESM 互操作
//     "skipLibCheck": true,          // 跳过 .d.ts 检查(提速)
//     "resolveJsonModule": true      // 允许 import JSON
//   }
// }

重要选项说明

| 选项 | 常用值 | 作用 |

| --- | --- | --- |

| target | ES2020/ESNext | 编译到的语法版本 |

| module | ESNext/CommonJS | 输出模块系统 |

| moduleResolution | Bundler/Node16 | 如何找模块 |

| paths + baseUrl | @/ → src/ | 路径别名 |

| lib | DOM/ES2020 | 内置 API 类型 |

| esModuleInterop | true | 允许 default 导入 CJS |

| skipLibCheck | true | 跳过依赖类型检查,编译提速可达 30% |

| declaration | true | 生成 .d.ts(库作者需要) |

| noEmit | true | 只类型检查不产出(配合 Babel/esbuild) |

常见坑:`paths` 只影响类型检查,不改变运行时解析——运行时还需打包器(Vite/Webpack)或 `tsconfig-paths` 配套映射,否则运行时找不到模块。

最佳实践:新项目直接 `strict: true`;`skipLibCheck: true` 提速;用 Vite/esbuild 时 `noEmit: true` + `isolatedModules: true`。

装饰器与框架实战类型

实验性装饰器 vs TC39 新标准

概念:装饰器是加在类、方法、属性上的特殊声明。TypeScript 早期实现的是"实验性装饰器"(需 `experimentalDecorators`),5.0 起支持 TC39 标准装饰器,二者签名不同。

typescriptCode
// TC39 标准装饰器(TS 5.0+,无需 experimentalDecorators)
function logged<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const name = String(context.name);
  return function (this: This, ...args: Args): Return {
    console.log(`调用 ${name},参数:`, args);
    return target.call(this, ...args);
  };
}

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

两种装饰器对比

| 维度 | 实验性装饰器 | TC39 标准装饰器 |

| --- | --- | --- |

| 配置 | experimentalDecorators | 5.0+ 默认 |

| 参数签名 | target, key, descriptor | value, context |

| 参数装饰器 | 支持 | 尚不支持 |

| metadata | 配 reflect-metadata | context.metadata |

| 生态(NestJS等) | 大量依赖 | 逐步迁移 |

与 React 结合的类型

typescriptCode
import { useState, type ReactNode, type FC } from 'react';

// 组件 props 类型
interface CardProps {
  title: string;
  children: ReactNode;      // 可渲染内容
  onClose?: () => void;     // 可选回调
}

const Card: FC<CardProps> = ({ title, children, onClose }) => {
  const [open, setOpen] = useState<boolean>(true); // 泛型指定 state 类型
  if (!open) return null;
  return null; // 示意
};

// 事件类型
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value);
}

// 泛型组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
  return items.map(renderItem);
}

与 Vue 结合的类型

typescriptCode
import { defineComponent, ref, type PropType } from 'vue';

interface Todo { id: number; text: string; done: boolean; }

export default defineComponent({
  props: {
    // 用 PropType 标注复杂类型
    todos: { type: Array as PropType<Todo[]>, required: true },
    title: { type: String, default: '待办' },
  },
  setup(props) {
    const count = ref<number>(0); // ref 泛型
    const addCount = () => { count.value++; };
    return { count, addCount };
  },
});

类型兼容性、协变与逆变

概念:TypeScript 用结构化类型判断兼容性。协变(Covariance)指"子类型关系随成员方向保持一致",逆变(Contravariance)指"方向相反",主要体现在函数参数上。

typescriptCode
// 结构兼容:只要结构满足即可赋值
interface Point2D { x: number; y: number; }
interface Point3D { x: number; y: number; z: number; }
let p2: Point2D;
let p3: Point3D = { x: 1, y: 2, z: 3 };
p2 = p3; // OK:Point3D 结构上包含 Point2D(多的属性无妨)
// p3 = p2; // Error:缺少 z

// 协变:返回值、数组元素
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
let animals: Animal[];
let dogs: Dog[] = [{ name: 'a', bark: () => {} }];
animals = dogs; // 协变:Dog[] 可赋给 Animal[]

// 逆变:函数参数(strictFunctionTypes 下)
type AnimalHandler = (a: Animal) => void;
type DogHandler = (d: Dog) => void;
let handleAnimal: AnimalHandler = (a) => console.log(a.name);
let handleDog: DogHandler = handleAnimal; // OK:参数逆变,接受更宽的处理器
// handleAnimal = handleDog; // Error:handleDog 需要 bark,但传入的可能只是 Animal

协变/逆变速记表

| 位置 | 型变 | 记忆 |

| --- | --- | --- |

| 函数返回值 | 协变 | 返回更具体 OK |

| 函数参数 | 逆变 | 接受更宽泛 OK |

| 数组元素(读) | 协变 | Dog[] → Animal[] |

| 可变数组(写) | 本应不变 | TS 放宽为协变(有坑) |

| 方法参数 | 双变 | TS 为易用性放宽 |

常见坑

1.TS 出于易用性,方法(method 语法)参数是"双变"的,不如函数属性(`(x)=>`)严格,可能漏掉类型错误。
2.数组协变 + 可写会制造不安全:`animals.push({name:'x'})` 实际往 `dogs` 塞了没有 bark 的对象。
3.忘开 `strictFunctionTypes`,函数参数不做逆变检查,回调类型不安全。

最佳实践:回调类型用函数属性写法而非方法写法以获得更严格检查;只读数组用 `readonly T[]` 避免协变写入坑;始终开 `strict`。

常见坑与最佳实践总览

| 坑 | 后果 | 对策 |

| --- | --- | --- |

| 滥用 any | 类型安全全失 | 换 unknown + 收窄 |

| 到处 as 断言 | 骗过编译器埋运行时坑 | 用类型守卫 |

| let 字面量宽化 | 类型意外变宽 | as const / 显式标注 |

| 数字枚举可赋任意数 | 越界值不报错 | 用字符串枚举/字面量联合 |

| Partial 只浅层 | 嵌套未变可选 | 自写 DeepPartial |

| paths 不影响运行时 | 打包后找不到模块 | 配打包器别名 |

| 忘 never 穷尽 | 新增分支漏处理 | default 赋值 never |

| 递归类型超深度 | 编译报错 | 改尾递归 |

小结表

| 主题 | 一句话 | 关键词 |

| --- | --- | --- |

| 模块 | ES Module 为主 | import type |

| 声明文件 | 给 JS 补类型 | declare、.d.ts |

| tsconfig | 控制编译行为 | strict、paths |

| 装饰器 | 元编程注解 | TC39 标准 |

| 框架类型 | props/ref/事件 | FC、PropType |

| 型变 | 结构兼容规则 | 协变、逆变 |

类型安全实战模式

前面的章节讲的是"零件",本节把它们拼成几个高频实战模式,直接可用于业务代码。

类型安全的 API 层

场景:前后端约定接口,用类型描述请求与响应,配合泛型封装 fetch,让每个接口调用都有精确的返回类型。

typescriptCode
// 统一响应包装
interface ApiResult<T> {
  code: number;
  data: T;
  message: string;
}

// 定义接口映射:路径 -> 响应数据类型
interface ApiMap {
  '/user/profile': { id: number; name: string; avatar: string };
  '/order/list': { orders: Array<{ id: string; total: number }> };
}

// 泛型请求函数:传入 path,自动推断返回类型
async function request<P extends keyof ApiMap>(
  path: P
): Promise<ApiResult<ApiMap[P]>> {
  const res = await fetch(path);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

// 使用:返回类型被精确推断,无需手动断言
async function demo() {
  const profile = await request('/user/profile');
  console.log(profile.data.name);   // string,IDE 有补全
  const orders = await request('/order/list');
  console.log(orders.data.orders[0].total); // number
}

类型安全的事件系统

场景:一个发布订阅中心,不同事件携带不同 payload,用映射类型保证 emit 和 on 的 payload 类型对齐。

typescriptCode
// 事件名 -> payload 类型
interface EventPayloads {
  login: { userId: number };
  logout: undefined;
  message: { from: string; text: string };
}

class TypedEmitter {
  private handlers: {
    [K in keyof EventPayloads]?: Array<(payload: EventPayloads[K]) => void>;
  } = {};

  on<K extends keyof EventPayloads>(
    event: K,
    handler: (payload: EventPayloads[K]) => void
  ): void {
    (this.handlers[event] ??= []).push(handler as any);
  }

  emit<K extends keyof EventPayloads>(event: K, payload: EventPayloads[K]): void {
    this.handlers[event]?.forEach(h => h(payload));
  }
}

const bus = new TypedEmitter();
bus.on('login', p => console.log(p.userId));   // p 类型精确
bus.emit('login', { userId: 1 });               // payload 校验
// bus.emit('login', { userId: 'x' });          // Error:userId 应为 number

用 satisfies 兼顾校验与推断(TS 4.9+)

概念:`satisfies` 检查表达式是否满足某类型,但不改变表达式本身被推断出的更窄类型。它解决了"用类型标注会丢失字面量精度、不标注又没校验"的两难。

typescriptCode
type RGB = [number, number, number];
type Palette = Record<string, RGB | string>;

// 用 : Palette 标注:green 会被拓宽成 RGB | string,丢失元组精度
const p1: Palette = { red: [255, 0, 0], green: '#00ff00' };
// p1.red 类型是 RGB | string,无法直接当元组用

// 用 satisfies:既校验满足 Palette,又保留每个值的精确类型
const p2 = {
  red: [255, 0, 0],
  green: '#00ff00',
} satisfies Palette;
p2.red[0];              // number,保留了元组类型
p2.green.toUpperCase(); // string,保留了字符串类型

satisfies vs as vs 类型标注对比

| 方式 | 是否校验 | 是否保留窄类型 | 风险 |

| --- | --- | --- | --- |

| : Type(标注) | 是 | 否(拓宽) | 丢精度 |

| as Type(断言) | 否(强转) | 部分 | 骗编译器 |

| satisfies Type | 是 | 是 | 无 |

品牌类型(Branded Types)防混淆

场景:`userId` 和 `orderId` 都是 `string`,结构上完全兼容,容易传错。用品牌类型给它们打上"标记",编译期区分。

typescriptCode
// 交叉一个带唯一 tag 的类型,制造名义上的区别
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function asUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId) { /* ... */ }

const uid = asUserId('u_123');
getUser(uid);          // OK
// getUser('u_123');   // Error:普通 string 不能当 UserId
// getUser(orderId);   // Error:OrderId 不能当 UserId

常见坑

1.`satisfies` 不会像标注那样在赋值点固定类型,若后续要求变量整体是某类型仍需标注。
2.品牌类型是"伪名义类型",运行时其实还是原始值,断言函数要保证逻辑正确。
3.事件系统里 `handler as any` 是内部实现的必要妥协,对外 API 仍然类型安全。

最佳实践:API 层用泛型 + 接口映射;同型不同义的值用品牌类型;配置对象用 `satisfies` 保精度又校验。

小结表

| 模式 | 解决什么 | 关键技术 |

| --- | --- | --- |

| 泛型 API 层 | 接口返回类型精确 | keyof 映射 + 泛型 |

| 类型化事件系统 | payload 对齐 | 映射类型 |

| satisfies | 校验又保精度 | 4.9+ |

| 品牌类型 | 同型防混淆 | 交叉 tag |

总结

| 概念 | 一句话记忆 | 关键词 |

| --- | --- | --- |

| 超集 | 合法 JS 就是合法 TS | 类型擦除 |

| 类型推断 | 能不写就不写 | 初始值、上下文 |

| 类型断言 | "我比编译器懂",慎用 | as、!、as const |

| any/unknown | unknown 是安全版 any | 收窄 |

| interface vs type | 对象用 interface,联合用 type | extends、& |

| 泛型 | 类型的参数化 | T、extends、keyof |

| 工具类型 | 从已有类型派生新类型 | Partial、Pick、Omit、Record |

| 类型守卫 | 联合类型的收窄 | typeof、in、is、never |

| 高级类型 | 类型层面编程 | 条件、映射、模板字面量 |

| strict 模式 | 严格才安全 | strictNullChecks |

一句话:TypeScript 的价值不在于"写更多类型",而在于用尽量少的标注,换取编译器帮你在写代码时就发现错误。