Exploring TypeScript Type Annotations - Type Checking
作者: zhilidali
欢迎来到 《探索 TypeScript 类型注解》 系列教程。
上一篇介绍了如何创建自定义类型,
本篇深入探索 TypeScript 编译器的静态类型检查机制。
目录
正文
当数据类型操作不合理时,编译器静态编译时会提示错误。
let obj = { a: 1 };
// A. 编译器推断出 obj 的类型: let obj: { a: number; }
// B. 编译器检查数据的使用是否合理,不合理时会抛出 Error
obj.b; // Error: 属性 b 不存在
obj.a = '2'; // Error:'2' 不能赋值给 number 类型
通过上面编译器对类型注解的静态检查,可以初步了解到类型检查的机制:编译器会适当的推断数据的类型,以及检查类型的使用是否合理,下面我们对类型检查机制进一步探索。
类型推断
Type Inference 类型推断:当没有显式指定类型注解时,编译器会推断出一个类型。
- Basic Type Inference 基本类型推断
- Best Common Type 最佳通用类型
- Contextual Typing 上下文类型
基本类型推断
在定义变量、设置函数参数默认值、函数返回值时,编译器都会自动进行类型推断:
// 在变量初始化时推断
let foo;
// let foo: any
let bar = 'ts';
// let bar: string
// 推断函数参数和返回值类型
let a = (x = 1) => {}
// let a: (x?: number) => void
类型推断在一定程度上可以保持代码简洁易读,但有时并不总是如此
let foo;
let f = foo;
// let f: undefined
let b: string | number = 'TS';
let baz = b;
// let baz: string
最佳通用类型
从多个表达式的类型推断出最佳通用类型
let foo = ['ts', 1];
// let foo: (string | number)[]
// string | number 为联合类型,描述数据可以是其中的一种类型
上下文类型
由上下文推断出表达式的类型
let add: (a: number, b: number) => number;
add = function(x, y) {
// 由 add 可推断出 x、y 以及返回值的类型
return x + y;
};
// add = function(x: number, y: number): number {
// return x + y;
// };
类型断言
Type Assertion 类型断言可以让你告诉编译器当前数据的类型,有两种语法:
- 尖括号语法:
<T>value
- as 语法:
value as T
(jsx 中只能用 as 语法)
interface Foo {
foo: number;
}
let a = {};
// let a: {}
let b = {} as Foo;
// let b: Foo; 类型断言告诉编译器 b 的类型为 Foo
a.foo; // Error
(a as Foo).foo; // Ok
b.foo; // Ok
类型兼容
如果类型 Y 可以赋值给类型 X,即 X = Y,那么我们说,目标类型 X 兼容源类型 Y。
interface TypeA {
a: string;
}
interface TypeB {
a: string;
}
let foo: TypeA = {a: 'ts'}
// 基于结构类型,所以 foo 可赋值给 bar,即 TypeB 兼容 TypeA
let bar: TypeB = foo;
TS 基于结构子类型设计 (因为 JS 中广泛的使用匿名对象,例如函数表达式和对象字面量),与名义类型形成对比。
如上因为 TypeA 与 TypeB 结构相同 (属性及其类型),所以在结构类型系统中,它们是兼容的。
而在名义类型系统中,类型的兼容性或等价性是基于声明和名称来决定的。
基本类型兼容
之前“数据类型”篇幅中已有介绍,详情请点击此处 。
// `string` 兼容 `null`; `null` 是 `string` 的子类型
let str: string = null;
成员结构兼容
必选成员少的兼容成员多的,即源类型至少具有与目标类型相同的成员
基本结构兼容
let foo: { a: string } = { a: 'a' };
let bar: { a: string, b: string } = { a: 'a', b: 'b' };
foo = bar; // OK, 少兼容多
bar = foo; // Error
// 函数返回值
let foo = () => ({ a: 'a' });
let bar = () => ({ a: 'a', b: 'b' });
foo = bar; // Ok, 少兼容多
bar = foo; // Error
类兼容
类的实例成员少的兼容成员多的 (比较两个类的对象时,静态成员不比较)
class A {
foo: number = 1
}
class B {
foo: number = 2
bar: number = 3
}
class C extends A {
baz: number = 4;
}
let a = new A();
let b = new B();
let c = new C();
a = b; // Ok, 少兼容多
b = a; // Error
c = a; // Error
a = c; // Ok, 少兼容多
枚举兼容
枚举类型与数值类型相互兼容,枚举之间不兼容
enum E { A, B }
enum F { X, Y }
// 枚举类型与数值类型相互兼容
let foo: E = 123; // OK, 枚举类型兼容数值类型
let bar: number = E.A; // OK, 数值类型兼容枚举类型
// 枚举之间不兼容
let baz = E.A;
baz = F.X; // Error
泛型兼容
// 类型成员都为空,相互兼容
interface Empty<T> {}
let x: Empty<number> = {};
let y: Empty<string> = {};
x = y; // OK
y = x; // OK
// 成员类型不同,所以相互不兼容
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number> = { data: 1 };
let y: NotEmpty<string> = { data: '1' };
x = y; // Error
y = x; // Error
函数参数个数兼容
多兼容少
- 参数多的兼容参数少的 (主要是因为 JS 中经常可以忽略多余的参数,比如 Array#map),
- 当 strictFunctionTypes 为 true 时,参数少的不能兼容参数多的
必选参数
let foo = (a: number) => a;
let bar = (a: number, b: number) => a + b;
foo = bar; // Error, 当 strictFunctionTypes 为 true 时
bar = foo; // OK, 多兼容少
必选参数、可选参数、剩余参数
let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => {}
a = b; // Ok
a = c; // Ok
b = a; // Error, 当 strictFunctionTypes 为 true 时
b = c; // Error, 当 strictFunctionTypes 为 true 时
c = a; // Ok
c = b; // Ok
命名参数
let foo = (p: { a: number }) => p.a;
let bar = (p: { a: number; b: number }) => p.a;
foo = bar; // Error when strictFunctionTypes is true
bar = foo; // OK
类型保护
许多表达式可以确保在某个作用域中运行时,数据有着更精确的类型,称为类型保护。
常识别为类型保护的操作符
- 相等运算符:
===
!==
- 逻辑运算符:
||
- 类型断言运算符:
!
去除 null 和 undefined
function getLength(arg: string | null): number {
arg.length; // Error,编译器检查到 arg 的类型为 string 或 null,所以不能访问 length 属性
if (arg === null) {
return 0; // 编译器可以推断出,在这个 if 子句中,arg 的类型是 null
}
return arg.length; // 编译器可以推断出,此处 arg 的类型是 string
}
类型谓词
函数的返回值类型为类型谓词 parameterName is Type
, 其中 parameterName
为函数参数。
// arg is string 是类型谓词
function isString(arg: any): arg is string {
return typeof arg === 'string';
}
function f(foo: string | string[]) {
if (isString(foo)) {
foo.toUpperCase(); // OK
} else if (Array.isArray(foo)) {
foo.map(it => it.toUpperCase()); // OK
}
}
可识别为类型保护的关键字
如下三个 JS 关键字可以帮助 TS 进一步识别类型
-
typeof:
type v === typename
其中 typename 为 boolean, number, string, symbol 时,TS 才会识别为类型保护。 - instanceof
- in
function f(foo: string | string[]) {
foo.toUpperCase(); // Error
foo.map(it => it.toUpperCase());
foo.toFixed(); // Error
if (typeof foo === 'string') {
foo.toUpperCase(); // OK
} else if (foo instanceof Array) {
foo.map(it => it.toUpperCase()); // OK
}
}
function b(bar: { a: string } | { b: string }) {
bar.a; // Error
bar.b; // Error
if ('a' in bar) {
bar.a; // Ok
} else {
bar.b; // Ok
}
}
结语
本篇通过类型检查机制探索了类型检查的规则,下篇通过将探索 TypeScript 的高级类型。
协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
网友评论