说说 TypeScript 中的类型兼容性和类型保护?
问题解析
类型兼容性和类型保护是 TypeScript 类型系统的核心机制,考察候选人对结构类型系统、类型收窄以及各种类型保护手段的理解。
核心概念
- 类型兼容性:一个类型能否赋值给另一个类型
- 结构类型系统:基于结构而非名义的类型系统
- 类型保护:运行时检查收窄类型范围
- 类型谓词:
parameter is Type自定义类型保护 - 可赋值性:类型之间的赋值规则
详细解答
1. 类型兼容性
TypeScript 使用结构类型系统(Structural Type System),类型兼容性基于类型的结构而非名称。
1.1 基本规则
interface Named {
name: string;
}
class Person {
name: string = '';
}
let p: Named;
// ok,因为 Person 的结构满足 Named
p = new Person();
1.2 对象兼容性
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
let p2d: Point2D = { x: 0, y: 0 };
let p3d: Point3D = { x: 0, y: 0, z: 0 };
// 可以,Point3D 包含 Point2D 的所有属性
p2d = p3d;
// 不可以,Point2D 缺少 z 属性
// p3d = p2d; // Error
规则:
- 源类型必须包含目标类型的所有必需属性
- 额外的属性不影响兼容性
1.3 函数兼容性
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // ok,忽略额外参数
// x = y; // Error,缺少参数
参数规则:
- 目标函数的参数可以少于源函数(忽略额外参数)
- 目标函数的参数不能多于源函数
返回值规则:
let x = () => ({ name: 'Alice' });
let y = () => ({ name: 'Alice', location: 'Seattle' });
x = y; // ok,返回值包含所需属性
// y = x; // Error,缺少 location 属性
1.4 枚举兼容性
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
// status = Color.Green; // Error,不同枚举不兼容
1.5 类兼容性
class Animal {
feet: number = 0;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number = 0;
constructor(numFeet: number) {}
}
let a: Animal = new Animal('', 0);
let s: Size = new Size(0);
a = s; // ok,结构相同
s = a; // ok,结构相同
注意:类的私有成员和受保护成员会影响兼容性。
2. 类型保护
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。
2.1 typeof 类型保护
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
// padding 被收窄为 number 类型
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
// padding 被收窄为 string 类型
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
支持的类型:number、string、boolean、symbol、undefined、function、object
2.2 instanceof 类型保护
class Bird {
fly() {
console.log('flying');
}
}
class Fish {
swim() {
console.log('swimming');
}
}
function move(pet: Bird | Fish) {
if (pet instanceof Bird) {
pet.fly(); // pet 被收窄为 Bird
} else {
pet.swim(); // pet 被收窄为 Fish
}
}
2.3 in 类型保护
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // animal 被收窄为 Bird
} else {
animal.swim(); // animal 被收窄为 Fish
}
}
2.4 字面量类型保护
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size; // s 被收窄为 Square
case "rectangle":
return s.width * s.height; // s 被收窄为 Rectangle
}
}
2.5 自定义类型保护(类型谓词)
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // pet 被收窄为 Fish
} else {
pet.fly(); // pet 被收窄为 Bird
}
}
类型谓词:parameter is Type,返回 boolean 的函数,用于自定义类型保护。
2.6 可辨识联合(Discriminated Unions)
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// 穷尽检查
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
3. 类型兼容性规则总结
// 1. 基本类型
let str: string = "hello";
// let num: number = str; // Error,基本类型不兼容
// 2. 对象类型 - 基于结构
interface A { x: number; }
interface B { x: number; y: number; }
let a: A = { x: 1 };
let b: B = { x: 1, y: 2 };
a = b; // ok
// b = a; // Error
// 3. 函数参数 - 双变(Bivariant)
let fn1: (x: number) => void = (x) => {};
let fn2: (x: number, y: number) => void = (x, y) => {};
fn2 = fn1; // ok
// 4. 函数返回值 - 协变(Covariant)
let fn3: () => { x: number; } = () => ({ x: 1 });
let fn4: () => { x: number; y: number; } = () => ({ x: 1, y: 2 });
fn3 = fn4; // ok
// 5. 数组 - 协变
let arr1: Array<{ x: number; }> = [{ x: 1 }];
let arr2: Array<{ x: number; y: number; }> = [{ x: 1, y: 2 }];
arr1 = arr2; // ok
深入理解
严格类型检查
{
"compilerOptions": {
"strictFunctionTypes": true, // 严格函数类型检查
"strictNullChecks": true, // 严格 null 检查
"strictPropertyInitialization": true // 严格属性初始化
}
}
类型收窄的层次
function example(x: string | number | boolean) {
if (typeof x === "string") {
// x: string
x.toUpperCase();
} else if (typeof x === "number") {
// x: number
x.toFixed(2);
} else {
// x: boolean(穷尽)
x.valueOf();
}
}
最佳实践
- 利用类型保护收窄类型:避免使用类型断言
- 使用自定义类型保护:提高代码可读性和复用性
- 设计可辨识联合:使用
kind等字面量属性区分类型 - 穷尽检查:在 switch 中使用
never确保所有情况都被处理 - 开启严格模式:
strict: true获得更强的类型检查
面试要点
- 类型兼容性:基于结构而非名称,源类型必须包含目标类型的所有必需属性
- 对象兼容性:属性多的可以赋值给属性少的(协变)
- 函数兼容性:参数少的可以赋值给参数多的(逆变,双变)
- 类型保护方式:
typeof:基本类型检查instanceof:类实例检查in:属性存在检查- 字面量检查:可辨识联合
- 自定义类型保护:
is类型谓词
- 可辨识联合:使用共同的可辨识属性(如
kind)区分联合类型成员 - 穷尽检查:使用
never确保 switch 处理所有情况 - 严格类型检查:
strictFunctionTypes、strictNullChecks等配置