-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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 国际许可协议进行许可。
《探索 TypeScript 类型注解》
- To pick up a draggable item, press the space bar. While dragging, use the arrow keys to move the item. Press space again to drop the item in its new position, or press escape to cancel.
Activity