「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。
一、类型检查机制
类型检查机制:Typescript编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。
类型检查机制的作用:辅助开发,提高开发效率。
类型检查机制分类:
- 类型推断
- 类型兼容性
- 类型保护
二、类型推断
1. 类型推断的概念
类型推断不需要用户指定变量的类型(函数返回值的类型),Typescript可以根据某些规则自动地为其推断出一个类型。
2.类型推断的内容
类型推断包括:
- 基础类型推断
- 最佳通用类型推断
- 上下文类型推断
- 基础类型推断
let aa = 1; //number
上例中,ts会自动辨别出aa为number类型。
- 最佳通用类型推断
let bb = [1,null]; // number[]
上例中,ts自动推断类型为number[],当tsconfig.js文件中设置了"strictNullChecks": true,时,
ts自动推断类型为(number | null)[]
- 上下文类型推断
三、类型兼容
当一个类型Y可以被赋值给另一个类型X时,我们就可以说类型X兼容X兼容类型Y。
例:
let s:string = 's';
s = null;
1. 接口兼容性
接口是否兼容可以使用鸭式辨型法。 鸭式辨型法:只要有鸭子的某些特性就是鸭子。 原类型必须具备目标类型的必要属性,成员少的兼容成员多的。
举个例子,
interface X {
a:any;
}
interface Y {
a:any;
b:any;
}
let x1:X = {a:1}
let y1:Y = {a:1,b:2}
x1 = y1
上例中,Y类型拥有X类型中的所有属性方法,X类型兼容Y类型,所以y1可以赋值给x1。
2. 函数兼容性
(1)参数个数
函数参数数量只能比类型参数数量少,不能多。
type Handler = (a:number, b:number) => void;
function hof(handler: Handler) {
return handler;
}
let handler1 = (a:number) => {}
hof(handler1);
let handler2 = (a:number,b:number, c:number)=>{}
hof(handler2);//报错
上例中,handler1的参数个数为1,比Handler类型定义的参数数量少,所以兼容。 handler2的参数个数为2,比Handler类型定义的参数数量多,所以不兼容。
(2)可选参数和剩余参数
固定参数可以兼容可选参数和剩余参数。 可选参数可以兼容固定参数和剩余参数。 剩余参数可以兼容固定参数和可选参数。
let fun11 = (p1:number, p2:number) => {}
let fun22 = (p1?:number, p2?:number) => {}
let fun33 = (...args:number[]) => {}
fun11 = fun22;
fun11 = fun33;
fun22 = fun11;
fun22 = fun33;
fun33 = fun11;
fun33 = fun22;
上例中,fun11是固定参数,fun22包括可选参数,fun3包括剩余参数。
由于固定参数可以兼容可选参数和剩余参数,所以fun11兼容fun22和fun33。
由于固定参数可以兼容可选参数和剩余参数,所以fun22兼容fun11和fun33。
由于固定参数可以兼容可选参数和剩余参数,所以fun33兼容fun22和fun11。
注意:若可选参数可以兼容固定参数和剩余参数,剩余参数可以兼容固定参数和可选参数,出现错误时,修改tsconfig.js中strictFunctionTypes为false。
(3)参数类型
- 参数的类型必须一致才能兼容。
- 参数多的兼容参数少的。
例:
type Handler = (a:number, b:number) => void;
function hof(handler: Handler) {
return handler;
}
let handler3 = (a:string) => {}
hof(handler3); //报错
上例中,handler3的参数a的类型与预定义的类型Handler中的参数类型不一致,所以不兼容。
例:
interface Print3D {
x:number;
y:number;
z:number;
}
interface Print2D {
x:number;
y:number;
}
let p3d = (print:Print3D) => {}
let p2d = (print:Print2D) => {}
p3d = p2d
p2d = p3d // 报错
上例中定义了p3d函数参数的类型包含三个参数, p2d函数参数的类型包含两个参数,由于参数多的兼容参数少的,所以p3d兼容p2d,但p2d不兼容p3d。
(4)返回值类型 返回值类型相同或为其子类型,则兼容。 例:
let f = ()=>({name: 'Amy'})
let g = ()=>({name: 'Amy',age:12})
f = g
g = f // 报错
(5)函数重载 对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
源函数指具体实现。 目标函数指类型定义。
// 目标函数
function overload(a:number,b:number):number;
function overload(a:string,b:string):string;
// 源函数
function overload(a:any,b:any):any{};
3.枚举兼容
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。
enum Fruit { Apple, Banana }
enum Color { Red, Blue }
let fruit: Fruit.Apple;
let num: number = fruit;
let color: Color.Red = fruit; // 报错
上例中,变量num定义为number类型,能兼容Fruit.Apple类型,但是Color.Red不能兼容Fruit.Apple类型。
4. 类兼容
(1)类中构造函数和静态成员是不进行比较的。
class A {
constructor(p:number, q:number) { }
id:number = 1;
}
class B {
static s = 1;
constructor() {}
id:number = 2;
}
let a11 = new A(1,2);
let b11 = new B();
a11 = b11;
b11 = a11;
上例中,类A和B中的构造函数和静态成员不进行比较,两个类中都只有number类型的id属性,所以两个类的实例是兼容的。
(2)若B类中比A类中多属性,则A类的实例兼容B类的实例,但是B类的实例无法兼容A类的实例。
class A {
constructor(p:number, q:number) { }
id:number = 1;
}
class B {
static s = 1;
constructor() {}
id:number = 2;
name: string = '';
}
let a11 = new A(1,2);
let b11 = new B();
a11 = b11;
b11 = a11;//报错
上例中,若B类中比A类中多了个name属性,则A类的实例兼容B类的实例,但是B类的实例无法兼容A类的实例。
(3)如果类中有私有或受保护成员时,则两个无关的类是不兼容的,只有父类和之间可以相互兼容。
class C {
private id:number = 1;
}
let c11 = new C();
c11 = a11 // 报错
a11 = c11 // 报错
class D extends C {
name:string = '';
}
let d11 = new D();
c11 = d11;
d11 = c11; // 报错
上例中,类C包含私有属性,类A与C毫无关系,因此两个类的实例不能相互兼容。D类是C类的子类,所以C类的实例兼容D类的实例。但由于D类比C类中多了一个name属性,所以D类的实例不能兼容C类的实例。
5.泛型兼容性
(1)当类型参数没有被内部使用时,不存在兼容性问题。
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // ok
(2)当类型参数被内部成员使用时,会影响兼容性。
interface Empty<T> {
data: T;
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // 报错
(3)对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。
let identity = function<T>(x: T): T { }
let reverse = function<U>(y: U): U { }
identity = reverse;