返回首页

说说你对 TypeScript 中高级类型的理解?有哪些?

问题解析

本题考察对 TypeScript 高级类型特性的掌握程度,包括交叉类型、联合类型、映射类型、条件类型等。这些特性是 TypeScript 类型系统的核心,能够应对复杂多变的开发场景。

核心概念

  • 交叉类型(Intersection Types):将多个类型合并为一个类型
  • 联合类型(Union Types):表示值可以是多种类型之一
  • 类型别名(Type Aliases):为类型创建新名字
  • 类型索引(keyof):获取对象类型的所有键
  • 类型约束(extends):对泛型进行约束
  • 映射类型(Mapped Types):基于旧类型创建新类型
  • 条件类型(Conditional Types):根据条件选择类型

详细解答

1. 是什么

除了 stringnumberboolean 这种基础类型外,在 TypeScript 类型声明中还存在一些高级的类型应用。

这些高级类型,是 TypeScript 为了保证语言的灵活性,所使用的一些语言特性。这些特性有助于我们应对复杂多变的开发场景。

2. 高级类型详解

2.1 交叉类型(Intersection Types)

通过 & 将多个类型合并为一个类型,包含了所需的所有类型的特性,本质上是一种并的操作。

语法T & U

适用于对象合并场景:

function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for (let key in first) {
    (<any>result)[key] = (<any>first)[key];
  }
  for (let key in second) {
    if (!result.hasOwnProperty(key)) {
      (<any>result)[key] = (<any>second)[key];
    }
  }
  return result;
}

// 使用示例
interface Person {
  name: string;
}
interface Contact {
  phone: string;
}

const personWithContact: Person & Contact = {
  name: "Tom",
  phone: "123456789"
};

2.2 联合类型(Union Types)

联合类型的语法规则和逻辑"或"的符号一致,表示其类型为连接的多个类型中的任意一个,本质上是一个交的关系。

语法T | U

function formatCommandline(command: string[] | string) {
  let line = '';
  if (typeof command === 'string') {
    line = command.trim();
  } else {
    line = command.join(' ').trim();
  }
  return line;
}

// 使用示例
let value: string | number;
value = "hello"; // ok
value = 123;     // ok
// value = true; // error

2.3 类型别名(Type Aliases)

类型别名会给一个类型起个新名字,类型别名有时和接口很像,但是可以作用于原始值、联合类型、元组以及其它任何你需要手写的类型。

// 基础类型别名
type some = boolean | string;
const b: some = true;      // ok
const c: some = 'hello';   // ok
// const d: some = 123;    // 不能将类型"123"分配给类型"some"

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

// 自引用类型别名
type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
};

// 与接口的区别
// interface 只能用于定义对象类型
type Point = {
  x: number;
  y: number;
};

// type 可以定义联合类型、交叉类型、原始类型等
type ID = string | number;
type Coordinates = Point & { z: number };

interface vs type

  • interface 只能用于定义对象类型
  • type 的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围更加广泛

2.4 类型索引(keyof)

keyof 类似于 Object.keys,用于获取一个接口中 Key 的联合类型。

interface Button {
  type: string;
  text: string;
}

type ButtonKeys = keyof Button;
// 等效于
type ButtonKeys = "type" | "text";

// 实际应用
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const button: Button = { type: "primary", text: "Click" };
const typeValue = getProperty(button, "type"); // string

2.5 类型约束(extends)

通过关键字 extends 进行约束,不同于在 class 后使用 extends 的继承作用,泛型内使用的主要作用是对泛型加以约束。

type BaseType = string | number | boolean;

// 参数只能是字符串、数字、布尔这几种基础类型
function copy<T extends BaseType>(arg: T): T {
  return arg;
}

// 类型约束通常和类型索引一起使用
function getValue<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]; // ok
}

const obj = { a: 1, b: "hello" };
const a = getValue(obj, 'a'); // 1
const b = getValue(obj, 'b'); // "hello"
// const c = getValue(obj, 'c'); // Error: 类型"c"的参数不能赋给类型"a" | "b"的参数

2.6 映射类型(Mapped Types)

通过 in 关键字做类型的映射,遍历已有接口的 key 或者是遍历联合类型。

// 内置的 Readonly 类型实现
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface Obj {
  a: string;
  b: string;
}

type ReadOnlyObj = Readonly<Obj>;
// 等效于:
// interface ReadOnlyObj {
//   readonly a: string;
//   readonly b: string;
// }

// 其他内置映射类型
type Partial<T> = {
  [P in keyof T]?: T[P];
};

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

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

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

映射类型的执行步骤

  1. keyof T:通过类型索引 keyof 得到联合类型 'a' | 'b'
  2. P in keyof T 等同于 P in 'a' | 'b',相当于执行了一次 forEach 的逻辑

2.7 条件类型(Conditional Types)

条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。

语法T extends U ? X : Y

意思是:如果 TU 的子集,就是类型 X,否则为类型 Y

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

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

// 实际应用:根据类型选择处理函数
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

// 条件类型与映射类型结合
type NonNullable<T> = T extends null | undefined ? never : T;

3. 高级类型的实际应用

工具类型实现

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

// Nullable:可为 null
type Nullable<T> = T | null;

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

// Parameters:获取函数参数类型
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

深入理解

infer 关键字

infer 用于在条件类型中声明类型变量,用于推断类型。

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

type NumArray = number[];
type Num = ElementType<NumArray>; // number

// 提取 Promise 的返回值类型
type PromiseType<T> = T extends Promise<infer U> ? U : never;

type PromiseString = Promise<string>;
type Str = PromiseType<PromiseString>; // string

分布式条件类型

当条件类型作用于联合类型时,会分布式地应用于联合类型的每个成员。

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

// 分布式展开
type StrOrNumArray = ToArray<string | number>;
// 等效于:string[] | number[]

// 阻止分布式行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type ArrayOfStrOrNum = ToArrayNonDist<string | number>;
// 等效于:(string | number)[]

最佳实践

  1. 优先使用内置工具类型:TypeScript 提供了丰富的内置工具类型(PartialRequiredReadonlyPickRecord 等)
  2. 合理使用类型别名:复杂类型使用 type 定义,提高可读性
  3. 善用条件类型:处理动态类型场景,如 API 响应类型转换
  4. 避免过度复杂化:类型是为了提高开发效率,过于复杂的类型会降低可维护性

面试要点

  1. 交叉类型 &:合并多个类型,取并集
  2. 联合类型 |:可以是多种类型之一
  3. 类型别名 type:vs 接口,适用范围更广
  4. keyof:获取对象所有键的联合类型
  5. extends:类型约束和条件类型
  6. 映射类型:基于旧类型创建新类型,如 Readonly<T>Partial<T>
  7. 条件类型T extends U ? X : Y,支持 infer 推断
  8. 内置工具类型:熟悉 PartialRequiredPickRecordReturnType 等的实现原理