Skip to main content

进阶

在高级前端开发和源码阅读(如 Vue3、React、Ant Design 等库的 TS 源码)中,经常会遇到极其复杂的类型定义。本章节总结了高级面试中常考的 TS 进阶特性和类型兼容性原理。

1. 条件类型 (Conditional Types)

条件类型的语法与三元表达式非常相似:T extends U ? X : Y。 它的含义是:如果类型 T 可以赋值给类型 U(即 TU 的子类型),那么返回类型 X,否则返回类型 Y

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

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

分发条件类型 (Distributive Conditional Types)

【面试高频】当条件类型作用于一个泛型类型且给定一个联合类型时,它会变成分发的。

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

// 期待的结果可能是 (string | number)[]
// 但实际结果是 string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

底层执行过程:(string extends any ? string[] : never) | (number extends any ? number[] : never)

如何阻止分发? 用方括号 [] 将泛型包裹起来:

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

2. 协变 (Covariance) 与逆变 (Contravariance)

这是 TypeScript 类型兼容性中最难懂但也最能拉开面试差距的概念。

  • 协变(Covariance):允许子类型转换为父类型。(“你要求我提供一个动物,我给你一只猫,这是可以的”)
  • 逆变(Contravariance):允许父类型转换为子类型。

对象的协变

TypeScript 中的对象是协变的。包含多余属性的类型(子类型)可以赋值给属性较少的类型(父类型):

interface Animal {
name: string;
}
interface Cat {
name: string;
meow: () => void;
}

let animal: Animal;
let cat: Cat = { name: "Tom", meow: () => {} };

animal = cat; // ✅ 协变(Cat 是 Animal 的子类型)

函数参数的逆变(难点)

函数的参数类型是逆变的(在开启了 strictFunctionTypes 的情况下)

假设有两个函数:

let fn1: (a: Animal) => void;
let fn2: (c: Cat) => void;

// fn2 期待一只猫(里面可能会调用 c.meow())
// fn1 期待一个动物(只会调用 a.name)

fn2 = fn1; // ✅ 安全(逆变)。传给 fn2 的必定是 Cat,fn1 拿 Cat 去读 .name 是绝对安全的。
fn1 = fn2; // ❌ 报错。如果允许,调用 fn1 传个普通 Animal 进去,fn2 却试图调用 animal.meow(),就会在运行时报错!

总结:函数的返回值是协变的,而函数的参数是逆变的。这就是著名的“参数逆变,返回值协变”

3. 模板字面量类型 (Template Literal Types)

TypeScript 4.1 引入了模板字面量类型,允许在类型层面使用类似 ES6 模板字符串的语法进行字符串的拼接和推导。

type World = "world";
type Greeting = `hello ${World}`; // "hello world"

配合联合类型,会产生笛卡尔积式的组合:

type Color = "red" | "blue";
type Quantity = "one" | "two";
type Combinations = `${Color}-${Quantity}`;
// "red-one" | "red-two" | "blue-one" | "blue-two"

配合 infer 使用 (类型提取)

模板字面量与 infer 结合,可以非常方便地进行字符串层面的推导(例如提取 Vuex/Redux 的 getter 路径):

// 提取 "get:xxx" 中的 "xxx"
type ExtractActionName<T> = T extends `get:${infer Action}` ? Action : never;

type Action = ExtractActionName<"get:userInfo">; // "userInfo"

4. 声明合并 (Declaration Merging)

在 TS 中,interface 是可以同名定义的,TS 编译器会自动将它们合并。这在扩展全局对象时非常常用。

扩展 Window 对象

// 假设我们在全局引入了一个三方库,往 window 挂载了 __MY_STORE__
declare global {
interface Window {
__MY_STORE__: any;
}
}

// 这样使用就不会报错了
window.__MY_STORE__ = {};

扩展第三方模块

import "axios";

declare module "axios" {
export interface AxiosRequestConfig {
// 扩展一个自定义配置项
retryCount?: number;
}
}

熟练掌握这些进阶特性,不仅能在面对复杂的类型推导时游刃有余,也能在高级前端岗位的面试中展现出扎实的基础。