06 - 高级类型

2 阅读5分钟

本章学习 TypeScript 类型系统的高级特性,掌握这些就能应对大多数复杂场景。


6.1 联合类型(Union Types)

一个值可以是多种类型之一,用 | 连接:

let id: string | number;
id = "abc";  // ✅
id = 123;    // ✅
id = true;   // ❌

// 函数参数
function printId(id: string | number): void {
  // 只能使用两种类型共有的方法
  console.log(id.toString()); // ✅ 两者都有 toString

  // 需要缩小类型后才能用特有方法
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // ✅
  } else {
    console.log(id.toFixed(2)); // ✅
  }
}

可辨识联合(Discriminated Union)⭐

这是 TypeScript 中最常用的模式之一:

// 每个类型有一个共同的"标签"属性
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;    // TS 知道有 width 和 height
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

// 实际应用:Redux Action
type Action =
  | { type: "INCREMENT"; payload: number }
  | { type: "DECREMENT"; payload: number }
  | { type: "RESET" };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "INCREMENT":
      return state + action.payload;
    case "DECREMENT":
      return state - action.payload;
    case "RESET":
      return 0;
  }
}

6.2 交叉类型(Intersection Types)

& 将多个类型合并为一个类型:

type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };

type Person = HasName & HasAge & HasEmail;
// 等同于 { name: string; age: number; email: string }

const person: Person = {
  name: "张三",
  age: 25,
  email: "a@b.com",
};

Mixin 模式

type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };

type User = {
  id: number;
  name: string;
} & Timestamped & SoftDeletable;

// User 拥有所有属性

6.3 类型守卫(Type Guards)

类型守卫让你在运行时缩小类型范围

typeof 守卫

function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value; // padding: number
  }
  return padding + value; // padding: string
}

instanceof 守卫

class Dog {
  bark() { console.log("汪!"); }
}
class Cat {
  meow() { console.log("喵!"); }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();  // TS 知道是 Dog
  } else {
    animal.meow();  // TS 知道是 Cat
  }
}

in 守卫

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

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

自定义类型守卫(is)

interface Cat {
  type: "cat";
  meow(): void;
}
interface Dog {
  type: "dog";
  bark(): void;
}

// 返回类型 `animal is Cat` 是类型谓词
function isCat(animal: Cat | Dog): animal is Cat {
  return animal.type === "cat";
}

function handle(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // TS 知道是 Cat
  } else {
    animal.bark(); // TS 知道是 Dog
  }
}

断言函数(asserts)

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

function process(value: unknown) {
  assertIsString(value);
  // 从这里开始,value 的类型是 string
  console.log(value.toUpperCase());
}

6.4 类型缩小(Type Narrowing)

TypeScript 通过控制流分析自动缩小类型:

function example(x: string | number | boolean) {
  if (typeof x === "string") {
    x; // string
  } else if (typeof x === "number") {
    x; // number
  } else {
    x; // boolean
  }
}

// 真值检查
function printName(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase()); // string(排除了 null 和 undefined)
  }
}

// 相等检查
function compare(a: string | number, b: string | boolean) {
  if (a === b) {
    a; // string(唯一两者都可能的类型)
    b; // string
  }
}

6.5 条件类型(Conditional Types)

类似三元运算符,但用于类型:

// 语法:T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// 实际用途:根据输入推断输出
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;    // string
type B = Flatten<number[][]>;  // number[]
type C = Flatten<boolean>;     // boolean

infer 关键字

infer 用于在条件类型中"推断"出某个类型:

// 提取函数返回类型(ReturnType 的实现原理)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = MyReturnType<() => string>;         // string
type B = MyReturnType<(x: number) => boolean>; // boolean

// 提取 Promise 包裹的类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;           // number

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

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

分布式条件类型

当联合类型传入条件类型时,会自动"分发"到每个成员:

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

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

// 如果不想分发,用方括号包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]

6.6 映射类型(Mapped Types)

基于已有类型创建新类型,遍历每个属性:

// 语法:{ [K in keyof T]: 新类型 }

// 把所有属性变成可选(Partial 的实现原理)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// 把所有属性变成只读(Readonly 的实现原理)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 把所有属性的值变成 boolean
type Flags<T> = {
  [K in keyof T]: boolean;
};

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

type UserFlags = Flags<User>;
// { name: boolean; age: boolean; email: boolean }

键的重映射(as)

// 给所有属性名加前缀
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

type UserStrings = StringProps<User>;
// { name: string; email: string }

去除修饰符

// - 去除可选
type Required<T> = {
  [K in keyof T]-?: T[K];
};

// - 去除只读
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

6.7 模板字面量类型

将字符串字面量类型进行组合:

type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

// 自动生成所有组合
type CSSClass = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green"
// | "medium-red" | "medium-blue" | "medium-green"
// | "large-red" | "large-blue" | "large-green"

// 事件名生成
type EventName<T extends string> = `on${Capitalize<T>}`;

type MouseEvents = EventName<"click" | "mousedown" | "mouseup">;
// "onClick" | "onMousedown" | "onMouseup"

内置字符串类型工具

type A = Uppercase<"hello">;     // "HELLO"
type B = Lowercase<"HELLO">;     // "hello"
type C = Capitalize<"hello">;    // "Hello"
type D = Uncapitalize<"Hello">;  // "hello"

6.8 索引访问类型

通过索引访问其他类型的子类型:

interface User {
  name: string;
  age: number;
  address: {
    city: string;
    street: string;
  };
}

type Name = User["name"];       // string
type Age = User["age"];         // number
type Address = User["address"]; // { city: string; street: string }
type City = User["address"]["city"]; // string

// 联合索引
type NameOrAge = User["name" | "age"]; // string | number

// 用 keyof 获取所有值类型
type UserValues = User[keyof User]; // string | number | { city: string; street: string }

// 数组元素类型
type Arr = string[];
type ArrElement = Arr[number]; // string

6.9 typeof 类型操作符

从值推断类型:

const config = {
  host: "localhost",
  port: 3000,
  debug: true,
};

// 从值中提取类型
type Config = typeof config;
// { host: string; port: number; debug: boolean }

// 常用于获取函数类型
function createUser(name: string, age: number) {
  return { name, age, id: Math.random() };
}

type CreateUserFn = typeof createUser;
// (name: string, age: number) => { name: string; age: number; id: number }

type UserType = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }

as const 断言

// 不用 as const
const colors = ["red", "green", "blue"]; // string[]

// 用 as const —— 变成只读的字面量类型
const colors2 = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]

type Color = (typeof colors2)[number]; // "red" | "green" | "blue"

// 对象也可以
const config = {
  endpoint: "/api",
  timeout: 3000,
} as const;
// { readonly endpoint: "/api"; readonly timeout: 3000 }

6.10 satisfies 操作符

TS 4.9+ 新增,检查类型的同时保留推断:

type Color = "red" | "green" | "blue";
type Theme = Record<string, Color | Color[]>;

// 用 satisfies:既检查类型,又保留字面量推断
const theme = {
  primary: "red",
  secondary: "blue",
  gradients: ["red", "green"],
} satisfies Theme;

theme.primary;    // 类型是 "red"(不是 string | string[])
theme.gradients;  // 类型是 ("red" | "green")[]

// 对比:如果用类型注解
const theme2: Theme = {
  primary: "red",
  secondary: "blue",
  gradients: ["red", "green"],
};

theme2.primary; // 类型是 Color | Color[](丢失了具体信息)

📝 练习

  1. 定义一个可辨识联合 Result<T>,成功时包含 data: T,失败时包含 error: string
  2. 写一个自定义类型守卫 isNonNull<T>(value: T | null | undefined): value is T
  3. 用条件类型实现 IsArray<T>:如果是数组返回 true,否则返回 false
  4. 用映射类型实现 Nullable<T>:所有属性值可以为 null
// 参考答案

// 1
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult(result: Result<string>) {
  if (result.success) {
    console.log(result.data);  // TS 知道有 data
  } else {
    console.log(result.error); // TS 知道有 error
  }
}

// 2
function isNonNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const values = [1, null, 2, undefined, 3];
const filtered = values.filter(isNonNull); // number[]

// 3
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>;  // true
type B = IsArray<number>;    // false

// 4
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

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

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