TS 是鸭子类型系统,只要两个对象结构一致,就认为是同一种类型,而不需要两者的实际类型有显式的继承关系。类型兼容的目的一切都是为了安全,多从安全角度去考虑,就能理解为什么有的时候少的能赋给多的,有的时候多的能赋给少的。
联合类型
两个类型,类型少的(基础类型)可以赋给类型多的(联合类型)
type NumOrStr = number | string; // 类型多let numOrStr: NumOrStr = 1; // ok 类型少 少的类型可以赋给多的
接口类型
两个接口之间,类型多的接口可以赋给类型少的接口
interface Animal {name: string;age: number;}interface Cat {name: string;age: number;color: string;}let animal!: Animal // 类型少let cat!: Cat // 类型多animal = cat // ok,类型多的接口可以赋给类型少的接口cat = animal // error,类型 "Animal" 中缺少属性 "color",但类型 "Cat" 中需要该属性。let cat2: Cat = { // 类型兼容只针对类型,这里直接赋值,不会出现类型兼容name: 'cat',age: 1,color: 'black',size: 'small' // error,“size”不在类型“Cat”中}let cat3 = {name: 'cat',age: 1,color: 'black',size: 'small'}cat = cat3 // ok,为了看类型是否兼容,这里赋值的时候就会先进行类型转换
类型断言
若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A。
换言之:要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可
// TypeScript只会比较看它们最终的结构,发现Animal 兼容 Catinterface Animal {name: string;}interface Cat {name: string;run(): void;}function testAnimal(animal: Animal) {// 允许 animal as Cat 是因为「父类可以被断言为子类」return (animal as Cat);}function testCat(cat: Cat) {// 允许 cat as Animal 是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,故「子类可以被断言为父类」return (cat as Animal);}
⭐函数
参数类型与数量
逆变(Contravariant):传递参数个数,允许少的赋给多的
let sum = (a: string, b: string): string => a + bconst sum2 = (a: string): string => aconst sum3 = (a: string, b: string): string => a + bconst sum4 = (a: string, b: string, c: string): string => a + b + c // 参数个数多了sum = sum2 // oksum = sum3 // oksum = sum4 // error,不能传多个参数,不安全
协变(Covariant):声明参数类型,允许多的赋给少的
let sum = (a: string | number): string => ''const sum2 = (a: string | number): string => ''const sum3 = (a: string | number | boolean): string => ''const sum4 = (a: string): string => '' // 类型是少的sum = sum2 // oksum = sum3 // oksum = sum4 // error,不能少传类型,不安全
双向协变(Bivariant):TypeScript 的“双向协变”,默认不允许。老版本允许双向协变,即函数参数既可以协变也可以逆变,这并不是类型安全的。
interface Person {name: string;}interface Student extends Person {grade: number;}// 第2个参数应该消费一个 Personfunction doIt(provider: () => Person,customer: (v: Person) => void,): void {const p = provider();customer(p);}const personProvider = () => ({ name: "James" } as Person);const studentProvider = () => ({ name: "Jack", grade: 7 } as Student);const personCustomer = (p: Person) => console.log(p.name);const studentCustomer = (p: Student) => console.log(`${p.name} in ${p.grade}`);doIt(personProvider, personCustomer); // OKdoIt(personProvider, studentCustomer);// Error:studentCustomer需要消费一个 Student,给的是Person,那 studentCustomer 中使用 p.grade 时就会不找到 grade 属性。doIt(studentProvider, personCustomer);// OKdoIt(studentProvider, studentCustomer); // Error:提供了一个 Student,但是 doIt() 是把它当作 Person 看待的(根据参数声明)
返回类型
协变(Covariant):返回个数,允许多的赋给少
let point2D!: {x: number;y: number;}let point3D!: {x: number;y: number;z: number;}point2D = point3D; // ok,返回个数多的可以赋给个数少的point3D = point2D; // error,返回个数少的不能赋给个数多的
逆变(Contravariant):返回类型,允许少的赋给多的
let sum!: (a: string) => string | numberlet sum2!: (a: string) => stringlet sum3!: (a: string) => string | number | booleansum = sum2 // ok,返回类型少的可以赋给返回类型多的sum = sum3 // error,返回类型多的不可以赋给返回类型少的
可选的和 rest 参数
可选的和 Rest 参数(任何数量的参数)都是兼容的,但是可选的和必选的在strictNullChecks:false下是兼容的:
let foo = (x: number, y: number) => {};let bar = (x?: number, y?: number) => {};let bas = (...args: number[]) => {};foo = bar = bas;bas = bar = foo;
枚举
- 枚举与数字类型相互兼容 ```typescript enum Status { Ready, Waiting }
let status = Status.Ready; let num = 0;
status = num; num = status;
- 来自于不同枚举的枚举变量,被认为是不兼容的:```typescriptenum Status {Ready,Waiting}enum Color {Red,Blue,Green}let status = Status.Ready;let color = Color.Red;status = color; // Error
类
- 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查。 ```typescript class Animal { feet: number; constructor(name: string, numFeet: number) {} }
class Size { feet: number; constructor(meters: number) {} }
let a: Animal; let s: Size;
a = s; // OK s = a; // OK
- 私有的和受保护的成员必须来自于相同的类。```typescriptclass Animal {protected feet: number;}class Cat extends Animal {}let animal: Animal;let cat: Cat;animal = cat; // okcat = animal; // okclass Size {protected feet: number;}let size: Size;animal = size; // ERRORsize = animal; // ERROR
泛型
TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。
如下例子中,T 对兼容性没有影响:
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // ok
当 T 被成员使用时,它将在实例化泛型后影响兼容性:
interface Empty<T> {data: T;}let x: Empty<number>;let y: Empty<string>;x = y; // Error
如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:
let identity = function<T>(x: T): T {// ...};let reverse = function<U>(y: U): U {// ...};identity = reverse; // ok, 因为 `(x: any) => any` 匹配 `(y: any) => any`
