类型检查机制
TypeScript编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。
- 作用:辅助开发,提高开发效率。
- 分类:
- 类型推断
- 类型兼容性
- 类型保护
类型推断
不需要指定变量的类型(函数返回值的类型),
TypeScript可以根据某些规则自动推断出一个类型。
-
基础类型推断
从右向左推断,根据表达式右侧的值,推断表达式左侧的类型。
-
变量类型
let a = 1; // let a: number let b = "string"; // let b: string -
函数默认参数
let x = (a = 1) => {}; // (parameter) a: number -
函数返回值
let x = (a = 1) => a + 1; // let x: (a?: number) => number
-
-
最佳通用类型推断
从右向左推断。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。 如果没有找到最佳通用类型的话,类型推断的结果为联合类型。
let c = [1, 2]; // let c: number[] let d = [1, null]; /* ** 在 strictNullChecks:true 时,let d: (number | null)[] ** 在 strictNullChecks:false 时,let d: number[] */ -
上下文类型推断
从左向右推断,发生在表达式的类型与所处的位置相关时。
window.onkeydown = (event: string) => {}; // error! window.onkeydown = (event: KeyboardEvent) => {}; // ok!- 使用场景:
- 函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。
- 上下文类型也会做为最佳通用类型的候选类型。
最佳类型候选者有 4 个:function createZoo(): Animal[] { return [new Rhino(), new Elephant(), new Snake()]; }Animal、Rhino、Elephant、Snake。
- 使用场景:
-
如果 ts 的推断不符合你的预期?
当你有自信比
ts更了解你的代码时,你可以用 类型断言 来覆盖ts的推论。let foo = {}; foo.bar = 1; // Property 'bar' does not exist on type '{}'.此时,我们可以为
foo添加一个类型断言。interface Foo { bar: number; } let foo = {} as Foo; foo.bar = 1; // ok!但是当我们去掉语句
foo.bar = 1;,也没有报错;但此时foo并没有履行Foo的约定。所以,我们还是应该在foo声明时直接指定为Foo类型。let foo: Foo = { bar: 1 };总结, 类型断言可以增加代码的灵活性,在改造旧代码时非常有效;但不能滥用,不熟悉上下文的情况下,会带来安全隐患。
类型兼容性
TS允许不同类型的变量在一定规则下相互赋值,增加语言的灵活性。
TS里的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。它正好与名义(nominal)类型形成对比。
-
快速记忆
-
公式化
当一个
类型 Y可以被赋值给另一个类型 X时,我们就可以说类型 X兼容类型 Y。X 兼容 Y : X(目标类型) = Y(源类型) -
口诀
结构之间兼容:成员少的兼容成员多的;
函数之间兼容:参数多的兼容参数少的。
原始类型和对象类型
TypeScript结构化类型系统的基本原则是,如果x想兼容y,那么y至少具有和x相同的属性。// strictNullChecks: false let aa = 1; let bb = null; aa = bb; let a = { name: "" }; let b = { name: "string" }; a = b;检查函数参数时使用相同的规则。
interface Named { name: string; } let y = { name: "", age: 1 }; function greet(n: Named) { console.log("hello" + n.name); } greet(y); -
接口
成员少的 兼容 成员多的。
interface X {
a: number;
b: number;
}
interface Y {
a: number;
b: number;
c: number;
}
let x: X = { a: 1, b: 2 };
let y: Y = { a: 1, b: 2, c: 3 };
x = y;
y = x; // error!
函数
判断两个函数是否兼容,通常发生在函数相互赋值的情况下。
type Handler = (a: number, b: number) => number;
function hof(handler: Handler) {
return handler;
}
在这个例子里,handler 作为目标函数,传参作为源函数。
想要 handler 兼容传参,需要满足三个条件。
-
参数个数:
目标函数 > 源函数
let handler1 = (a: number) => number; hof(handler1); // ok! let handler2 = (a: number, b: number, c: number) => number; hof(handler2); // error!关于 可选参数和剩余参数
- 固定参数 兼容 可选参数和剩余参数
- 可选参数 不兼容 固定参数和剩余参数
- 剩余参数 兼容 固定参数和可选参数
let a = (a: number, b: number) => number; let b = (a?: number, b?: number) => number; let c = (...args: number[]) => number; a = b; a = c; b = a; b = c; c = a; c = b; -
参数类型
参数类型必须要匹配。
let handler3 = (a: string) => string; hof(handler3); // error!参数类型复杂的情况(对象类型):将对象属性看成传参,套用参数个数规则,多的兼容少的。
interface point3D { a: number; b: number; c: number; } interface point2D { a: number; b: number; } let p3d = (value: point3D) => {}; let p2d = (value: point2D) => {}; p3d = p2d; p2d = p3d; // error! strictFunctionTypes: false,可以兼容。-
函数参数双向协变:
函数参数之间可以互相兼容。
首先我们需要知道,函数这一类型是逆变的。函数可以看成是一把操作的集合,子类参数
<Dog>比父类参数<Animal>拥有更多的属性/方法。因此<Animal>函数是<Dog>函数的子集,兼容性是相反的,是逆变。那为什么又会有双向协变呢?
为了维持结构化类型的兼容性,
TypeScript团队做了一个权衡 (trade-off)。保持了函数类型的双向协变性。但是我们可以通过设置编译选项--strictFunctionTypes: true来保持函数的逆变性而关闭协变性。
-
-
返回值类型
同 “ 结构之间兼容:成员少的兼容成员多的。”
let f = () => ({ a: number, }); let g = () => ({ a: number, b: number }); f = g; // OK! g = f; // error!-
函数重载
函数重载列表重的函数 要兼容 执行函数。
function overload(a: number, b: number): number; // 目标函数 function overload(a: string, b: string): string; // 目标函数 function overload(a: any, b: any): any {} // 源函数
-
枚举类型
枚举默认和数值完全兼容 枚举之间完全不兼容
enum Fruit {
Apple,
Banana = "banana",
}
enum Color {
Red,
Green,
}
let fruit1: Fruit.Apple = 1; // ok!
let fruit2: Fruit.Banana = 1; // error!
let color1: Color = Color.Red; // ok!
let color2: Color = Fruit.Apple; // error!
类
类和接口相似,但有一点不同:静态成员(
static)和构造函数不在比较的范围内。
class Fruit {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
name: string;
size: number = 0;
age: number;
}
class Clothes {
static len: number = 100;
constructor(name: string) {
this.name = name;
}
name: string;
size: number = 0;
}
let f = new Fruit("apple", 12);
let c = new Clothes("t-shirt");
f = c; // error!
c = f; // ok!
类中有私有成员(private),只能父类兼容子类(或相互兼容)。
class Fruit {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
name: string;
private size: number = 0;
}
class Apple extends Fruit {}
let a = new Apple("apple", 1);
f = a; // OK!
泛型
interface Empty<T> {}
let a: Empty<number>;
let b: Empty<string>;
a = b!; // OK, because y matches structure of x
上面代码里,x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同。
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error!
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成 any 比较。 然后用结果类型进行比较,比如。
let identity = function <T>(x: T): T {
return x;
};
let reverse = function <U>(y: U): U {
return y;
};
identity = reverse; // OK, because (x: any) => any matches (y: any) => any
类型保护
类型保护允许你使用更小范围下的对象类型。
-
现在有这样一个 demo👇
enum Type { Strong, Week, } class Java { helloJava() { console.log("hello java"); } } class JavaScript { helloJavaScript() { console.log("hello javascript"); } } function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); return lang; } getLanguage(Type.Strong); getLanguage(Type.Week);现在,我们想在
getLanguage里调用Java/JavaScript的 hello 方法。function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); if (lang.helloJava) { // error! lang.helloJava(); // error! } else { lang.helloJavaScript(); // error! } return lang; }Property 'helloJava'/'helloJavaScript' does not exist on type 'Java | JavaScript'.当然,我们可以通过 类型断言 来去掉错误。function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); if ((lang as Java).helloJava) { (lang as Java).helloJava(); } else { (lang as JavaScript).helloJavaScript(); } return lang; }但是我们要在每一处都添加类型断言,这显然不是合理的解决方案;
于是,我们就要启动 类型保护体制 了。
ts能够在特定的区块中保证变量属于某种确定的类型;在此区块中,可以泛型使用此类型的属性,调用此类型的方法。 -
instanceof
判断一个实例是否属于某个类
function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // instanceof if (lang instanceof Java) { lang.helloJava(); } else { lang.helloJavaScript(); } return lang; } -
in
判断一个属性是否属于某个对象
function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // in if ("helloJava" in lang) { lang.helloJava(); } else { lang.helloJavaScript(); } return lang; } -
typeof
判断一个变量的类型
function doSome(x: number | string) { if (typeof x === "string") { x.length; } else { x++; } return x; } -
保护函数
某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内
function isJava(lang: Java | JavaScript): lang is Java { // 类型谓词 return (lang as Java).helloJava !== undefined; } function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // 保护函数 if (isJava(lang)) { lang.helloJava(); } else { lang.helloJavaScript(); } return lang; }
TypeScript 基础系列
- Hello TypeScript(01)-- 环境搭建
- Hello Typescript(02)-- 枚举类型
- Hello Typescript(03)-- 对象类型接口
- Hello Typescript(04)-- 函数类型接口、混合类型接口、类接口
- Hello Typescript(05)-- 函数
- Hello Typescript(06)-- 类
- Hello Typescript(07)-- 类与接口的关系
- Hello Typescript(08)-- 泛型
- Hello Typescript(09)-- 类型推断、类型兼容性、类型保护
- Hello Typescript(10)-- 交叉类型、联合类型、索引类型、映射类型、条件类型