进阶
在高级前端开发和源码阅读(如 Vue3、React、Ant Design 等库的 TS 源码)中,经常会遇到极其复杂的类型定义。本章节总结了高级面试中常考的 TS 进阶特性和类型兼容性原理。
1. 条件类型 (Conditional Types)
条件类型的语法与三元表达式非常相似:T extends U ? X : Y。
它的含义是:如果类型 T 可以赋值给类型 U(即 T 是 U 的子类型),那么返回类型 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;
}
}
熟练掌握这些进阶特性,不仅能在面对复杂的类型推导时游刃有余,也能在高级前端岗位的面试中展现出扎实的基础。