类型守卫与高级类型

类型缩小

常见守卫包括 typeofinstanceofin、判等和自定义类型谓词。

type Success = { status: 'success'; data: string[] };
type Failure = { status: 'failure'; message: string };
type Result = Success | Failure;

function printResult(result: Result) {
  if (result.status === 'success') {
    console.log(result.data);
  } else {
    console.error(result.message);
  }
}

这种带有公共字面量字段的联合叫可辨识联合,适合请求状态、Reducer Action 和业务状态机。

映射类型

映射类型遍历一组键并生成新对象类型。

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

内置的 PartialRequiredReadonlyPickOmitRecord 都建立在这些能力上。

条件类型

type ElementType<T> = T extends readonly (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number>;   // number

infer 用于在条件类型中声明待推导类型。

条件类型的分发

当裸类型参数接收联合类型时,条件类型会逐项执行:

type ToArray<T> = T extends unknown ? T[] : never;
type Value = ToArray<string | number>; // string[] | number[]

用元组包裹两侧可以阻止分发:

type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Value = ToArrayNonDist<string | number>; // (string | number)[]

函数重载与联合类型

当不同输入对应不同输出时可以使用重载:

function normalize(value: string): string[];
function normalize(value: string[]): string[];
function normalize(value: string | string[]): string[] {
  return Array.isArray(value) ? value : [value];
}

如果输入与输出没有这种对应关系,优先使用联合类型,避免制造过多重载。

高频工具类型手写

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type MyExclude<T, U> = T extends U ? never : T;

type MyReturnType<T extends (...args: never[]) => unknown> =
  T extends (...args: never[]) => infer R ? R : never;

面试时不仅要写出结果,还要解释 keyof、映射遍历、条件分发和 infer 各自承担的作用。