TypeScript 高级类型:条件类型、映射类型与模板字面量类型

详解 TypeScript 高级类型系统:条件类型(Conditional Types)、映射类型(Mapped Types)、模板字面量类型(Template Literal Types)、 keyof/infer、类型体操实用场景。含 10+ 个可直接复用的类型工具。

TypeScript 的类型系统是一门图灵完备的语言:它支持条件分支、循环(递归)、变量声明(泛型),可以表达极其复杂的类型关系。本文从实用角度讲解高级类型,提供可直接复用的类型工具。


一、条件类型(Conditional Types)

条件类型是 TypeScript 类型系统的核心控制流机制,语法形式为 T extends U ? X : Y,类似于三元运算符。它允许类型根据输入类型的结构特征做出分支判断,这是实现复杂类型逻辑的基础。

当 T 可以赋值给 U 时,整个条件类型解析为 X,否则解析为 Y。这种能力使得我们可以编写"如果类型是字符串就怎样,如果是数字就怎样"这样的类型级逻辑,将运行时判断前移至编译期。

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

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

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

当条件类型作用于裸类型参数时,它会自动分布式地应用到联合类型的每个成员上。这是 TypeScript 条件类型最强大也最容易被忽视的特性。所谓"裸类型参数",指的是 T extends U 中的 T 没有被包裹在数组、元组或其他泛型中。

利用分布式条件类型,可以轻松实现联合类型成员的筛选、转换和过滤。例如,从一组混合类型中提取出所有字符串类型,或者在编译期移除掉 undefined 和 null。

type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// 分布式展开为 string[] | number[],而不是 (string | number)[]

type ExtractString<T> = T extends string ? T : never;
type OnlyStrings = ExtractString<string | number | boolean>;
// 结果:string

如果需要禁用分布式行为,只需将类型参数包装成元组形式:[T] extends [any] ? ... 这样条件类型就会将联合类型视为一个整体来判断。

infer 关键字(类型提取)

infer 关键字用于在条件类型的 extends 子句中声明一个类型变量,让编译器从已有的类型结构中推断并提取出某个部分。它是类型体操中最常用的工具之一,几乎所有内置的类型工具(如 ReturnType、Parameters)底层都依赖 infer。

// 提取 Promise 的返回值类型
type Awaited<T> = T extends Promise<infer R> ? R : T;
type Result = Awaited<Promise<string>>; // string

// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => number;
type R = ReturnType<Fn>; // number

// 提取数组元素类型
type ElementType<T> = T extends Array<infer E> ? E : never;
type E = ElementType<string[]>; // string

infer 在元组与数组中的应用

infer 不仅能提取单一类型,还能通过多位置推断来解构元组、函数的参数列表,甚至递归地推断嵌套结构。配合 TypeScript 对元组长度的精确追踪能力,可以实现对元组头部、尾部、任意位置的精确操作。

// 提取元组第一个元素
type Head<T extends readonly any[]> = T extends readonly [infer H, ...any[]] ? H : never;
type H1 = Head<[1, 2, 3]>; // 1

// 提取元组剩余元素
type Tail<T extends readonly any[]> = T extends readonly [any, ...infer R] ? R : never;
type T1 = Tail<[1, 2, 3]>; // [2, 3]

// 提取函数参数类型列表
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = Parameters<(a: string, b: number) => void>; // [string, number]

// 提取数组最后一个元素
type Last<T extends readonly any[]> = T extends readonly [...any[], infer L] ? L : never;
type L1 = Last<[1, 2, 3]>; // 3

never 过滤技巧

never 在 TypeScript 中既是底类型(没有值是它的超类型),又在联合类型中表现为"吸收律":T | never 等价于 T。这个特性结合分布式条件类型,可以实现在编译期过滤联合类型中的特定成员。

// 从联合类型中排除特定类型
type Exclude<T, U> = T extends U ? never : T;
type WithoutNumber = Exclude<string | number | boolean, number>;
// 结果:string | boolean

// 只保留可分配给某类型的成员
type Extract<T, U> = T extends U ? T : never;
type OnlyStringOrNumber = Extract<'a' | 1 | true | null, string | number>;
// 结果:'a' | 1

// 过滤对象中值为函数的属性
type NonFunctionKeys<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

interface Mixed {
  name: string;
  age: number;
  log(): void;
}
type DataKeys = NonFunctionKeys<Mixed>; // 'name' | 'age'

将属性映射为 never 后,通过索引访问 T[keyof T] 收集所有值类型,由于 never 在联合类型中被消除,最终只剩下非 never 的成员,这是实现属性过滤的经典手法。


二、映射类型(Mapped Types)

映射类型是 TypeScript 中批量操作对象属性的核心工具。它使用 in 关键字遍历 keyof T 产生的联合类型,为每个键生成一个新的属性声明。本质上,映射类型是对象类型的"批量处理器",可以在一次声明中将所有属性变为可选、只读、可选读,或彻底修改键名和值类型。

// 把所有属性变为可选
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// 把所有属性变为只读
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 只保留指定属性
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// 自定义:把对象的所有值转为 string
type Stringify<T> = {
  [K in keyof T]: string;
};

interface User {
  name: string;
  age: number;
}
type StringUser = Stringify<User>;
// { name: string; age: string; }

Key Remapping(as 子句)

TypeScript 4.1 引入了 as 子句,允许在映射过程中对键名进行重映射。这不仅包括模板字面量类型的键名变换,还能在重映射时将键映射为 never 来实现属性过滤。as 子句极大地扩展了映射类型的表达能力,使得键级操作与值级操作一样灵活。

// 为所有键添加前缀
type AddPrefix<T, P extends string> = {
  [K in keyof T as `${P}_${K & string}`]: T[K];
};

type PrefixedUser = AddPrefix<User, 'user'>;
// { user_name: string; user_age: number; }

// 移除键名中的特定前缀
type RemovePrefix<T, P extends string> = {
  [K in keyof T as K extends `${P}_${infer R}` ? R : never]: T[K];
};

type org_name = string;
type org_age = number;
interface Prefixed {
  org_name: string;
  org_age: number;
}
type Plain = RemovePrefix<Prefixed, 'org'>;
// { name: string; age: number; }

过滤属性(通过 never)

利用 as 子句和 never 的消去特性,可以在映射类型的键名层面精确过滤属性。当重映射后的键名为 never 时,该属性不会出现在结果类型中。这种方式比先映射再 Pick 更优雅,也更具扩展性。

// 只保留 string 类型的属性
type StringProperties<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  email: string;
}
type StringOnly = StringProperties<Person>;
// { name: string; email: string; }

// 过滤掉函数类型的属性
type RemoveMethods<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

递归映射(DeepReadonly、DeepRequired)

映射类型默认只对对象的顶层属性生效。要实现"深度"效果(递归作用于嵌套对象),需要在映射值时再次应用自身。递归映射类型是 TypeScript 类型体操中的经典模式,通过在条件分支中自引用来实现对任意深度对象结构的遍历。

需要注意的是,递归类型必须包含终止条件(通常是 extends object ? ... : T),否则会导致无限递归和编译器报错。

// 递归只读:所有层级都变为 readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// 递归必填:所有层级都变为 required
type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};

// 递归可选:所有层级都变为 optional
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Nested {
  user: {
    profile: {
      name: string;
      age?: number;
    };
  };
}

type FullyReadonly = DeepReadonly<Nested>;
// { readonly user: { readonly profile: { readonly name: string; readonly age?: number | undefined; }; }; }

三、模板字面量类型

模板字面量类型将 JavaScript 的模板字符串语法引入了类型系统,允许用类型参数拼接字符串。最基本的用法是将一个字符串字面量类型嵌入到另一个模板中,实现类型安全的字符串构造。

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

// 生成 CSS margin/padding 工具类类型
type Spacing = 'xs' | 'sm' | 'md' | 'lg';
type MarginClass = `m-${Spacing}` | `mx-${Spacing}` | `my-${Spacing}`;
// 'm-xs' | 'm-sm' | 'mx-xs' | 'mx-sm' | ...

模板字面量 + 联合类型组合

当模板字面量中的插值位置是联合类型时,TypeScript 会自动生成笛卡尔积式的所有组合。这一特性使得在类型级别生成枚举值列表成为可能,对于需要严格约束字符串格式的场景极其有用。

例如,生成一套完整的 CSS 间距工具类类型,或者生成所有可能的事件处理器名称。由于这个过程在编译期完成,IDE 可以提供精确的自动补全,避免了运行时的字符串拼写错误。

type Sides = '' | 'x' | 'y' | 't' | 'r' | 'b' | 'l';
type Size = '0' | '1' | '2' | '4' | '8' | '16';

// 生成所有 tailwind 风格的 margin 类名
type MarginClassFull = `m${Sides}-${Size}`;
// m-0 | m-1 | m-2 | ... | mx-0 | mx-1 | ... | mt-0 | mt-1 | ...

// 生成 CSS 属性名
type CSSProperty = 'margin' | 'padding';
type CSSDirection = 'Top' | 'Right' | 'Bottom' | 'Left';
type FullCSSProperty = `${CSSProperty}${CSSDirection}`;
// 'marginTop' | 'marginRight' | 'paddingTop' | ...

// 生成所有响应式变体
type Breakpoint = 'sm' | 'md' | 'lg';
type Responsive<T extends string> = T | `${Breakpoint}:${T}`;
type ResponsiveMargin = Responsive<MarginClassFull>;

Intrinsic String Manipulation Types

TypeScript 内置了四种基于模板的字符串处理类型,可以直接对字符串字面量类型进行大小写转换。它们底层由编译器直接实现,不依赖递归类型,因此性能和递归深度都优于手写实现。

// 首字母大写
type Capitalize<S extends string> = intrinsic;  // 内置
// 实际使用演示
type A = Capitalize<'hello'>;      // 'Hello'

// 首字母小写
type B = Uncapitalize<'Hello'>;    // 'hello'

// 全大写
type C = Uppercase<'hello'>;       // 'HELLO'

// 全小写
type D = Lowercase<'HELLO'>;       // 'hello'

// 实战:从 kebab-case 转为 camelCase
type CamelCase<S extends string> =
  S extends `${infer P}-${infer Q}`
    ? `${P}${Capitalize<CamelCase<Q>>}`
    : S;

type Result = CamelCase<'hello-world-foo'>; // 'helloWorldFoo'

四、实用类型工具集

以下是一组经过生产环境验证的高级类型工具,涵盖对象扁平化、路径导航、联合类型转换等高频场景。这些工具可以直接复制到项目中作为类型工具库使用。

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

// 2. RequiredBy — 只让部分属性必填
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// 3. NonNullableProperties — 移除所有 null/undefined
type NonNullableProperties<T> = {
  [K in keyof T]: NonNullable<T[K]>;
};

// 4. Flatten — 拍平嵌套对象
type Flatten<T> = T extends object ? { [K in keyof T]: T[K] } : T;

// 5. UnionToIntersection — 联合转交叉
type UnionToIntersection<U> = (
  U extends any ? (k: U) => void : never
) extends (k: infer I) => void ? I : never;

TupleToUnion — 元组转联合类型

将固定结构和离散值进行转换是类型系统的常见需求。TupleToUnion 可以将元组的元素提取为联合类型,这在处理配置数组的允许值时非常方便。

// 元组转联合
type TupleToUnion<T extends readonly any[]> = T[number];
type Colors = TupleToUnion<['red', 'green', 'blue']>;
// 'red' | 'green' | 'blue'

// 联合转数组(借助函数参数逆变)
type UnionToTuple<T, L = LastInUnion<T>> = [T] extends [never]
  ? []
  : [...UnionToTuple<Exclude<T, L>>, L];

type LastInUnion<U> = UnionToIntersection<
  U extends any ? (x: U) => void : never
> extends (x: infer L) => void ? L : never;

IsEqual — 精确类型相等判断

判断两个类型是否严格相等是类型体操的基础操作。简单的 T extends U ? true : false 会因为结构类型等价性而产生误判。更准确的做法是利用条件类型在协变位置的分配特性来检测严格的类型同一性。

type IsEqual<A, B> =
  (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
    ? true
    : false;

type T1 = IsEqual<string, string>;     // true
type T2 = IsEqual<string, 'hello'>;    // false
type T3 = IsEqual<{a: 1}, {a: 1}>;     // true
type T4 = IsEqual<any, string>;        // false (strict mode)

Path / PathValue — 对象路径导航

在深度嵌套的对象中,通过点符号路径来安全地访问属性是一种常见的运行时需求。Path 和 PathValue 类型可以在编译期验证路径字符串是否合法,并提供精确的类型推断。这对表单验证、状态管理库(如 Redux、Zustand)的类型安全非常有价值。

type Path<T, K extends keyof T = keyof T> =
  K extends string
    ? T[K] extends object
      ? K | `${K}.${Path<T[K]>}`
      : K
    : never;

type PathValue<T, P extends Path<T>> =
  P extends `${infer K}.${infer Rest}`
    ? K extends keyof T
      ? Rest extends Path<T[K]>
        ? PathValue<T[K], Rest>
        : never
      : never
    : P extends keyof T
      ? T[P]
      : never;

interface Data {
  user: {
    name: string;
    address: {
      city: string;
    };
  };
}

type DataPath = Path<Data>;
// 'user' | 'user.name' | 'user.address' | 'user.address.city'

type CityValue = PathValue<Data, 'user.address.city'>; // string

FlatArray — 递归拍平数组

处理嵌套数组的统一方式是递归地展开每一层,直到元素不再是数组为止。FlatArray 类型接受一个深度参数,允许控制拍平的层数。

type FlatArray<Arr, Depth extends number = 1> = {
  done: Arr;
  recur: Arr extends readonly (infer InnerArr)[]
    ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
    : Arr;
}[Depth extends -1 ? 'done' : 'recur'];

type F1 = FlatArray<[[1, 2], [3, 4]], 1>;   // [1, 2, 3, 4]
type F2 = FlatArray<[[[1]], [[2]]], 2>;     // [1, 2]
type F3 = FlatArray<[[[[1]]]], 3>;          // [1]

五、逆变与协变(Contravariance vs Covariance)

TypeScript 的类型系统基于结构类型而非名义类型,这意味着类型兼容性由其成员结构决定而非显式声明。在结构类型系统中,类型参数的位置(作为函数参数返回、作为对象属性读取)会决定其是协变、逆变还是双变。

**协变(Covariance)**意味着子类型关系保持方向一致:如果 Dog extends Animal,那么 Dog[] 兼容于 Animal[]。**逆变(Contravariance)**意味着方向反转:如果 Dog extends Animal,那么 (x: Animal) => void 兼容于 (x: Dog) => void,而非相反。这是因为一个接受 Animal 的函数一定也能接受 Dog,但接受 Dog 的函数未必能处理所有 Animal。

// 协变示例:数组元素类型
declare let animals: Animal[];
declare let dogs: Dog[];
animals = dogs; // OK,数组是协变的

// 逆变示例:函数参数类型
type Handler<T> = (x: T) => void;
declare let animalHandler: Handler<Animal>;
declare let dogHandler: Handler<Dog>;
dogHandler = animalHandler; // OK, Handler<Animal> 可赋值给 Handler<Dog>

readonly 修饰符对变型的影响

在 TypeScript 中,对象属性的可变性会影响其变型行为。带有 readonly 修饰符的属性在函数类型中被视为协变而非逆变。这是因为 readonly 承诺了"只读",消除了写入操作可能引发类型不安全的风险。

interface Getter<T> {
  get: () => T;
}

// 默认情况下,方法参数位置是双变的(bivariant)
// 开启 strictFunctionTypes 后变为严格逆变

interface Input<T> {
  handle: (value: T) => void;
}

// 在 strictFunctionTypes 关闭时,以下赋值可以通过编译
// 开启 strictFunctionTypes 后则报错,因为参数位置要求逆变

六、类型谓词(Type Predicates)与断言函数

类型谓词允许自定义函数在返回 true 时告诉编译器收窄类型。它通过 parameter is Type 语法声明,是处理联合类型时的核心工具。与 typeof 和 instanceof 等内置收窄机制不同,类型谓词让用户可以定义任意复杂的类型检查逻辑。

// 基本类型谓词
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function process(input: string | number) {
  if (isString(input)) {
    // input 在此处收窄为 string
    console.log(input.toUpperCase());
  } else {
    // input 在此处收窄为 number
    console.log(input.toFixed(2));
  }
}

// 对象类型谓词
interface Cat {
  type: 'cat';
  meow(): void;
}
interface Dog {
  type: 'dog';
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return animal.type === 'cat';
}

asserts 关键字

asserts 是断言函数的语法,用于声明"如果函数正常返回,则参数一定符合某类型"。与类型谓词返回布尔值不同,断言函数失败时会直接抛出异常,因此编译器可以在调用点之后无条件地收窄类型。这种机制在防御式编程和输入校验场景中极为实用。

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Expected string');
  }
}

function run(input: unknown) {
  assertIsString(input);
  // 从这里开始,input 的类型收窄为 string,无需额外判断
  console.log(input.length);
}

// asserts 与属性断言
type User = { name: string; email: string };
function assertUser(obj: unknown): asserts obj is User {
  if (!obj || typeof (obj as any).name !== 'string') {
    throw new Error('Not a valid User');
  }
}

七、Branded Types 与 Opaque Types

TypeScript 本质上采用结构类型系统,这意味着两个结构完全相同的类型被认为是等价的,即使它们在领域语义上代表完全不同的概念。例如,type UserId = string 和 type ProductId = string 在 TypeScript 中可以互相替换,这在大型系统中容易引发严重的类型安全漏洞。

**Branded Types(品牌类型)**和 Opaque Types(不透明类型) 是在结构类型系统中模拟名义类型的常用技术。它们通过在类型中添加一个不可见的标记属性,使得两个底层结构相同但标记不同的类型在编译期被视为互不兼容。

// Branded Type 基础模式
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;

function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

const userId = 'u-123' as UserId;
const productId = 'p-456' as ProductId;

getUser(userId);       // OK
getUser(productId);    // Error: ProductId 不能赋值给 UserId

Opaque Type 的严格实现

Branded Type 通过交叉类型实现,虽然标记属性在运行时不存在,但类型上是可以访问到的。如果希望更严格地隐藏内部实现,可以使用私有声明符号(unique symbol)搭配类型工具来构建无法从外部解构的 Opaque 类型。

declare const __opaque__: unique symbol;

// 使用条件类型隐藏 brand 属性
type Opaque<T, K> = T & { readonly [__opaque__]: K };

// 工厂函数:确保值通过校验后才获得类型
type Email = Opaque<string, 'Email'>;
function createEmail(value: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    throw new Error('Invalid email');
  }
  return value as Email;
}

const email = createEmail('test@example.com');
// email 的类型是 Email(名义类型),不能与普通 string 互换

八、TS 类型体操实战:实现一个完整的 EventEmitter 类型安全封装

类型体操的最终价值在于解决真实的工程问题。本节以一个类型安全的 EventEmitter 实现为例,综合运用条件类型、映射类型、infer 和模板字面量类型,展示如何让事件监听和触发在编译期就得到严格的类型校验。

核心思路是将事件名映射到对应的参数类型。通过泛型约束,确保 emit 方法的参数列表与事件定义中的参数类型一一对应,on 方法的回调函数参数也能自动推断。这种设计模式广泛应用于类型安全的 WebSocket 客户端、状态管理库和跨进程通信封装中。

// 事件定义接口
interface EventMap {
  'user:login': { userId: string; timestamp: number };
  'user:logout': { userId: string };
  'error': Error;
  'data': string;
}

// 类型安全的 EventEmitter 实现
type EventKey = string | symbol;

type EventCallback<T> = T extends any[]
  ? (...args: T) => void
  : (payload: T) => void;

class TypedEventEmitter<Events extends Record<EventKey, any>> {
  private listeners: {
    [K in keyof Events]?: Array<EventCallback<Events[K]>>;
  } = {};

  on<K extends keyof Events>(
    event: K,
    listener: EventCallback<Events[K]>
  ): this {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
    return this;
  }

  off<K extends keyof Events>(
    event: K,
    listener: EventCallback<Events[K]>
  ): this {
    const arr = this.listeners[event];
    if (arr) {
      this.listeners[event] = arr.filter((l) => l !== listener) as any;
    }
    return this;
  }

  emit<K extends keyof Events>(
    event: K,
    ...args: Events[K] extends any[] ? Events[K] : [Events[K]]
  ): boolean {
    const arr = this.listeners[event];
    if (!arr || arr.length === 0) return false;
    arr.forEach((listener) => {
      (listener as any)(...(Array.isArray(args[0]) ? args[0] : args));
    });
    return true;
  }

  once<K extends keyof Events>(
    event: K,
    listener: EventCallback<Events[K]>
  ): this {
    const onceWrapper = (...args: any[]) => {
      this.off(event, onceWrapper as any);
      (listener as any)(...args);
    };
    return this.on(event, onceWrapper as any);
  }
}

// 使用示例:严格的类型推导
const emitter = new TypedEventEmitter<EventMap>();

// 回调参数自动推断为 { userId: string; timestamp: number }
emitter.on('user:login', (data) => {
  console.log(data.userId);      // 可安全访问
  console.log(data.timestamp);   // 可安全访问
});

// 编译错误:事件名不存在
// emitter.on('unknown:event', () => {});

// 编译错误:参数类型不匹配
// emitter.emit('user:login', 'wrong-type');

emitter.emit('user:logout', { userId: 'u-001' });

此实现的关键在于:

  1. 泛型约束 Events extends Record<EventKey, any>:确保事件表是一个以字符串或符号为键的对象。
  2. EventCallback<Events[K]>:将事件值类型转换为回调函数的签名。
  3. ...args 的 variadic 推断:支持数组形式的多参数事件和对象形式的单参数事件。
  4. keyof Events 作为事件名:将所有键严格限定在定义的事件范围内。

九、TS 5.x 新特性

TypeScript 5.x 系列引入了若干对类型系统有深远影响的特性。这些新特性不仅提升了开发体验,还扩展了类型体操的工具箱,使得一些以往难以实现的模式变得简洁高效。

const Type Parameters

在 TypeScript 5.0 之前,调用泛型函数时传入的字面量会被推断为更宽泛的类型。const 类型参数修饰符(const T)改变了这一行为:字面量参数会被尽可能精确地推断为字面量类型,无需再手动写 as const。

// TS 5.0 之前:需要显式 as const
function createRoute(path: readonly string[]) { return path; }
const r1 = createRoute(['home', 'about', 'contact'] as const);

// TS 5.0 +:使用 const 类型参数
function createRoute2<const T extends readonly string[]>(path: T): T {
  return path;
}
const r2 = createRoute2(['home', 'about', 'contact']);
// r2 的类型精确推断为 readonly ['home', 'about', 'contact']

satisfies 运算符

satisfies 运算符是 TypeScript 4.9 引入的关键特性,它检查表达式是否满足指定类型的约束,同时保留表达式的原始推断类型。这解决了长期以来 as(类型断言,过于宽松)和显式注解(丢失精确推断)之间的矛盾。

type Config = {
  host: string;
  port: number;
};

// 使用 satisfies:host 必须是 string,port 必须是 number
// 但对象的键值推断不会丢失
declare const config: Config;

const settings = {
  host: 'localhost',
  port: 3000,
  debug: true,
} satisfies Config;

// settings.debug 仍然存在(类型为 boolean),不会被 Config 消费掉
// 如果 port 写成字符串,编译器会报错
// satisfies 在字面量类型推断中的威力
type Color = 'red' | 'green' | 'blue';
const palette = {
  primary: 'red',
  secondary: 'green',
} satisfies Record<string, Color>;

// palette.primary 的类型精确推断为 'red',而非 Color
// 如果写成 're'(拼写错误),编译器会报错

Decorators 类型支持

TypeScript 5.0 完整实现了 ECMAScript Decorators 提案,并提供了完善的类型支持。装饰器是一种元编程工具,能够在不修改原类定义的情况下为其附加行为。新的 decorator 语法更加简洁,同时类型系统可以精确推断装饰器对类、方法和属性的影响。

// 类装饰器
type Constructor = new (...args: any[]) => {};

function logged<T extends Constructor>(target: T) {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Instance created: ${target.name}`);
    }
  };
}

@logged
class User {
  constructor(public name: string) {}
}

const u = new User('Alice'); // 控制台输出: Instance created: User
// 方法装饰器(带类型推断)
function measure<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);
  return function (this: This, ...args: Args): Return {
    const start = performance.now();
    const result = target.call(this, ...args);
    const end = performance.now();
    console.log(`${methodName} took ${end - start} ms`);
    return result;
  };
}

class Calculator {
  @measure
  fibonacci(n: number): number {
    return n <= 1 ? n : this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

新版装饰器利用 ClassMethodDecoratorContext 等内置类型,既能保留被装饰方法的完整签名(包括 this 类型和返回值),又能在装饰器中注入横切关注点(如日志、权限校验、性能监控),类型安全且无需反射元数据依赖。


相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. GraphQL BFF 与微前端:多前端团队的 Schema 分片与协作模式
  2. 前端 GraphQL 客户端集成:Apollo Client、Relay 与 urql 选型
  3. 前端性能调试实战:从首屏加载到运行时瓶颈