0%

TypeScript类型检查机制

类型检查机制

TypeScript编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。

作用:辅助开发,提高开发效率。

  • 类型推断
  • 类型兼容性
  • 类型保护

类型推断

不需要指定变量的类型(函数返回值的类型),TypeScript可以根据某些规则自动的为其推断出一个类型。

  • 基础类型推断
  • 最佳通用类型推断
  • 上下文类型推断

基础类型推断,最佳通用类型推断 都是从右往左的推断,根据表达式右侧的值来推测表达式左侧变量的类型。

上下文类型推断是从左往右的推断,通常出现在 事件处理 中。

类型推断不符合你的要求的时候,你可以使用 类型断言 as

类型断言 可以增加代码的灵活性,在改造旧代码时非常有效,但是类型断言要 避免滥用 ,要对自己 上下文 充足的 预判没有任何根据的类型断言,会给你的代码 安全隐患

基础类型推断

这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

1
2
3
4
5
6
7
8
// 初始化变量 x:number
let x = 3

// 设置默认参数值 x:number
let y = (x=1)=>{}

// 确定函数返回值 z:number
let z = (x = 1) => { x + 1}

最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,

1
let x = [0, 1, null]

为了推断 x 的类型,我们必须考虑所有元素的类型。 这里有两种选择:number 和 null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

由于最终的通用类型取自候选类型,有些时候候选类型共享一个公共结构,但是却没有一个类型能做为所有候选类型的超级类型。例如:

1
2
3
4
5
6
7
8
9
10
11
class Animal {
numLegs: number
}

class Dog extends Animal {
}

class Lion extends Animal {
}

let zoo = [new Dog(), new Lion()]

这里,我们想让 zoo 被推断为 Animal[] 类型,但是这个数组里没有对象是 Animal 类型的,因此不能推断出这个结果。 为了更正,我们可以明确的声明我们期望的类型:

1
let zoo: Animal[] = [new Dog(), new Lion()]

如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Dog | Lion)[]

上下文类型推断

有些时候,TypeScript 类型推断会按另外一种方式,我们称作 上下文类型;上下文类型的出现和表达式的类型以及所处的位置相关。通常出现在 事件处理 中,比如:

1
2
3
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.clickTime) // Error
}

这个例子会得到一个类型错误,TypeScript 类型检查器使用 window.onmousedown 函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent 参数的类型了,所以 mouseEvent 访问了一个不存在的属性,就报错了。

如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。重写上面的例子:

1
2
3
window.onmousedown = function(mouseEvent:any) {
console.log(mouseEvent.clickTime) // OK
}

这个函数表达式有明确的参数类型注解,上下文类型被忽略。这样的话就不报错了,因为这里不会使用到上下文类型。

上下文类型会在很多情况下使用到。通常包含函数的参数,赋值表达式的右边,类型断言,对象成员,数组字面量和返回值语句。上下文类型也会做为最佳通用类型的候选类型。比如:

1
2
3
4
5
function createZoo(): Animal[] {
return [new Bee(), new Lion()]
}

let zoo = createZoo()

这个例子里,最佳通用类型有 3 个候选者:Animal,Bee 和 Lion。 其中,Animal 会被做为最佳通用类型。

类型兼容性

当一个 类型Y(源类型)可以被赋值给另一个 类型X(目标类型) 时,可以说类型X兼容类型Y。

源类型必须具备目标类型的 必要属性

口诀:

  • 结构之间兼容:成员少的兼容成员多的
  • 函数之间兼容:参数多的兼容参数少的

TypeScript允许在类型兼容的变量之间相互赋值,这个特性增加了语言的 灵活性

接口兼容性

接口之间相互兼容时,成员少的可以兼容成员多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 接口兼容性
interface X {
a: any;
b: any;
}
interface Y {
a: any;
b: any;
c: any;
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
x = y
// Property 'c' is missing in type 'X' but required in type 'Y'.
y = x

函数兼容性

判断两个函数是不是兼容,通常发生在两个函数相互赋值的情况下,也就是函数作为参数的情况下。当给下面的高阶函数 hof 传递 参数 时,就会判断 参数Handler 是不是类型兼容。Handler 是目标类型参数 是源类型

要目标类型,兼容源类型需要满足3个条件:

  • 1)参数个数
    • 目标函数的参数个数要多于源函数的参数个数
    • 固定参数可以兼容剩余参数,可选参数
    • 可选参数不兼容固定参数 ,剩余参数。可以设置 strictFunctionTypes:false 实现兼容
    • 剩余参数兼容固定参数,可选参数
  • 2)参数类型
    • 目标函数的参数类型要包含源函数的参数类型
  • 3)函数返回值
    • 成员少的可以兼容多的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 函数兼容性
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
return handler
}

// 1)参数个数
let handler1 = (a: number) => { }
hof(handler1)
let handler2 = (a: number, b: number, c: number) => { }
// 类型“(a: number, b: number, c: number) => void”的参数不能赋给类型“Handler”的参数。
// hof(handler2)

// 可选参数和剩余参数
let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => { }
a = b
a = c
// 可选参数不兼容固定参数 ,剩余参数
// b = a
// b = c
c = a
c = b

// 2)参数类型
let handler3 = (a: string) => { }
// hof(handler3)

interface Point3D {
x: number;
y: number;
z: number;
}
interface Point2D {
x: number;
y: number;
}
let p3d = (point: Point3D) => { }
let p2d = (point: Point2D) => { }
p3d = p2d
// 成员少的可以兼容多的
// p2d = p3d

// 3) 返回值类型
let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
// 不能将类型“() => { name: string; }”分配给类型“() => { name: string; location: string; }”。Property 'location' is missing in type '{ name: string; }' but required in type '{ name: string; location: string; }'.
// g = f

函数重载兼容性

函数重载包括两个部分,一部分是重载列表,一部分是具体实现。列表中的函数是目标函数具体实现是源函数。程序运行时,编译器会查找重载列表,使用第一个匹配的定义,来执行源函数。

在函数重载中,目标函数参数个数多于或者等于源函数参数个数。并且返回值也要兼容目标函数返回值。

1
2
3
4
5
6
7
8
9
10
11
// 函数重载 
// 重载列表
function overload(a: number, b: number): number
function overload(a: string, b: string): string
// 具体实现
function overload(a: any, b: any): any { }
// function overload(a: any): any {}
// This overload signature is not compatible with its implementation signature.
// function overload(a: any, b: any, c: any): any {}
// This overload signature is not compatible with its implementation signature.
// function overload(a: any, b: any) {}

枚举类型兼容性

枚举类型和数字类型是完全兼容的。枚举之间是完全不兼容的。

1
2
3
4
5
6
7
// 枚举兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
let fruit: Fruit.Apple = 1
let no: number = Fruit.Apple
// 不能将类型“Fruit.Apple”分配给类型“Color.Red”。
let color: Color.Red = Fruit.Apple

类兼容性

比较两个类的兼容性时,类的构造函数和静态成员不参与比较。

如果两个类具有相同的实例成员,它们的实例就可以完全相互兼容。

当类中有私有成员时,这两个类就不相互兼容了,只有父类和子类之间可以相互兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类兼容性
class A {
constructor(p: number, q: number) { }
id: number = 1
private name: string = ''
}
class B {
static s = 1
constructor(p: number) { }
id: number = 2
private name: string = ''
}
class C extends A { }
let aa = new A(1, 2)
let bb = new B(1)
// 不能将类型“B”分配给类型“A”。类型具有私有属性“name”的单独声明。
aa = bb
// 不能将类型“A”分配给类型“B”。类型具有私有属性“name”的单独声明。
bb = aa
let cc = new C(1, 2)
aa = cc
cc = aa

泛型兼容性

泛型接口中,只有泛型变量被接口成员使用才影响泛型的兼容性

1
2
3
4
5
6
7
8
// 泛型接口
interface Empty<T> {
value: T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// 不能将类型“Empty<string>”分配给类型“Empty<number>”。不能将类型“string”分配给类型“number”。
obj1 = obj2

上面如果 泛型变量 T 如果没有被成员变量使用则兼容

1
2
3
4
5
6
7
// 泛型接口
interface Empty<T> {
// value: T //没有使用泛型变量 T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
obj1 = obj2

如果两个泛型函数的定义相同,但是没有指定类型参数,那么它们之间也是可以互相兼容的。

1
2
3
4
5
6
7
8
9
let log1 = <T>(x: T): T => {
console.log('x')
return x
}
let log2 = <U>(y: U): U => {
console.log('y')
return y
}
log1 = log2

类型保护

TypeScript能够在特定的区块中保证变量属于某种确定的类型。

可以在此区块中引用此类型的属性,或者调用此类型的方法。

问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Type { Strong, Week }

class JavaScript {
helloJavaScript() {
console.log('Hello JavaScript')
}
js: any
}

function getLanguage(type: Type) {
let lang = type === Type.Strong ? new Java() : new JavaScript();

// 类型“Java | JavaScript”上不存在属性“helloJava”。类型“JavaScript”上不存在属性“helloJava”。
// if (lang.helloJava) {
// lang.helloJava();
// } else {
// // 类型“Java | JavaScript”上不存在属性“helloJavaScript”。类型“Java”上不存在属性“helloJavaScript”。
// lang.helloJavaScript();
// }
// 类型断言
if ((lang as Java).helloJava){
(lang as Java).helloJava();
}else{
(lang as JavaScript).helloJavaScript();
}
}

getLanguage(Type.Week)

上面的代码使用类型断言,代码可读性很差,使用 类型保护 机制可以解决这个问题,可以提前预判变量的类型。

可以通过下面的方法解决:

  • instanceof:判断实例是不是属于某个类
  • in:判断一个属性是否属于某个对象
  • typeof:判断一个变量的类型
  • 类型保护函数:某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
enum Type { Strong, Week }

class Java {
helloJava() {
console.log('Hello Java')
}
java: any
}

class JavaScript {
helloJavaScript() {
console.log('Hello JavaScript')
}
js: any
}

// 类型保护函数
// lang is Java:类型谓词
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined
}

function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();

if (isJava(lang)) {
lang.helloJava();
} else {
lang.helloJavaScript();
}

// if ((lang as Java).helloJava) {
// (lang as Java).helloJava();
// } else {
// (lang as JavaScript).helloJavaScript();
// }

// instanceof
// if (lang instanceof Java) {
// lang.helloJava()
// // lang.helloJavaScript()
// } else {
// lang.helloJavaScript()
// }

// in
// if ('java' in lang) {
// lang.helloJava()
// } else {
// lang.helloJavaScript()
// }

// typeof
// if (typeof x === 'string') {
// console.log(x.length)
// } else {
// console.log(x.toFixed(2))
// }

return lang;
}

getLanguage(Type.Week, 1)

参考

-------------本文结束感谢您的阅读-------------