本文整理自网络,侵删。
TypeScript 2.0介绍
Null和undefined类型
TypeScript现在有两个特殊的类型:Null和Undefined, 它们的值分别是null
和undefined
。 以前这是不可能明确地命名这些类型的,但是现在 null
和undefined
不管在什么类型检查模式下都可以作为类型名称使用。
以前类型检查器认为null
和undefined
赋值给一切。实际上,null
和undefined
是每一个类型的有效值, 并且不能明确排除它们(因此不可能检测到错误)。
--strictNullChecks
--strictNullChecks
可以切换到新的严格空检查模式中。
在严格空检查模式中,null
和undefined
值不再属于任何类型的值,仅仅属于它们自己类型和any
类型的值 (还有一个例外, undefined
也能赋值给void
)。因此,尽管在常规类型检查模式下T
和T | undefined
被认为是相同的 (因为 undefined
被认为是任何T
的子类型),但是在严格类型检查模式下它们是不同的, 并且仅仅 T | undefined
允许有undefined
值,T
和T | null
的关系同样如此。
示例
// 使用--strictNullChecks参数进行编译的
let x: number;
let y: number | undefined;
let z: number | null | undefined;
x = 1; // 正确
y = 1; // 正确
z = 1; // 正确
x = undefined; // 错误
y = undefined; // 正确
z = undefined; // 正确
x = null; // 错误
y = null; // 错误
z = null; // 正确
x = y; // 错误
x = z; // 错误
y = x; // 正确
y = z; // 错误
z = x; // 正确
z = y; // 正确
使用前赋值检查
在严格空检查模式中,编译器要求未包含undefined
类型的局部变量在使用之前必须先赋值。
示例
// 使用--strictNullChecks参数进行编译
let x: number;
let y: number | null;
let z: number | undefined;
x; // 错误,使用前未赋值
y; // 错误,使用前未赋值
z; // 正确
x = 1;
y = null;
x; // 正确
y; // 正确
编译器通过执行基于控制流的类型分析检查变量明确被赋过值。在本篇文章后面会有进一步的细节。
可选参数和属性
可选参数和属性会自动把undefined
添加到他们的类型中,即使他们的类型注解明确不包含undefined
。例如,下面两个类型是完全相同的:
// 使用--strictNullChecks参数进行编译
type T1 = (x?: number) => string; // x的类型是 number | undefined
type T2 = (x?: number | undefined) => string; // x的类型是 number | undefined
非null和非undefined类型保护
如果对象或者函数的类型包含null
和undefined
,那么访问属性或调用函数时就会产生编译错误。因此,对类型保护进行了扩展,以支持对非null和非undefined的检查。
示例
// 使用--strictNullChecks参数进行编译
declare function f(x: number): string;
let x: number | null | undefined;
if (x) {
f(x); // 正确,这里的x类型是number
}
else {
f(x); // 错误,这里的x类型是number?
}
let a = x != null ? f(x) : ""; // a的类型是string
let b = x && f(x); // b的类型是 string | 0 | null | undefined
非null和非undefined类型保护可以使用==
、!=
、===
或!==
操作符和null
或undefined
进行比较,如x != null
或x === undefined
。对被试变量类型的影响准确地反映了JavaScript的语义(比如,双等号运算符检查两个值无论你指定的是null还是undefined,然而三等于号运算符仅仅检查指定的那一个值)。
类型保护中的点名称
类型保护以前仅仅支持对局部变量和参数的检查。现在类型保护支持检查由变量或参数名称后跟一个或多个访问属性组成的“点名称”。
示例
interface Options {
location?: {
x?: number;
y?: number;
};
}
function foo(options?: Options) {
if (options && options.location && options.location.x) {
const x = options.location.x; // x的类型是number
}
}
点名称的类型保护和用户定义的类型保护函数,还有typeof
和instanceof
操作符一起工作,并且不依赖--strictNullChecks
编译参数。
对点名称进行类型保护后给点名称任一部分赋值都会导致类型保护无效。例如,对x.y.z
进行了类型保护后给x
、x.y
或x.y.z
赋值,都会导致x.y.z
类型保护无效。
表达式操作符
表达式操作符允许运算对象的类型包含null
和/或undefined
,但是总是产生非null和非undefined类型的结果值。
// 使用--strictNullChecks参数进行编译
function sum(a: number | null, b: number | null) {
return a + b; // 计算的结果值类型是number
}
&&
操作符添加null
和/或undefined
到右边操作对象的类型中取决于当前左边操作对象的类型,||
操作符从左边联合类型的操作对象的类型中将null
和undefined
同时删除。
// 使用--strictNullChecks参数进行编译
interface Entity {
name: string;
}
let x: Entity | null;
let s = x && x.name; // s的类型是string | null
let y = x || { name: "test" }; // y的类型是Entity
类型扩展
在严格空检查模式中,null
和undefined
类型是不会扩展到any
类型中的。
let z = null; // z的类型是null
在常规类型检查模式中,由于扩展,会推断z
的类型是any
,但是在严格空检查模式中,推断z
是null
类型(因此,如果没有类型注释,null
是z
的唯一值)。
非空断言操作符
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符!
可以用于断言操作对象是非null和非undefined类型的。具体而言,运算x!
产生一个不包含null
和undefined
的x
的值。断言的形式类似于<T>x
和x as T
,!
非空断言操作符会从编译成的JavaScript代码中移除。
// 使用--strictNullChecks参数进行编译
function validateEntity(e?: Entity) {
// 如果e是null或者无效的实体,就会抛出异常
}
function processEntity(e?: Entity) {
validateEntity(e);
let s = e!.name; // 断言e是非空并访问name属性
}
兼容性
这些新特性是经过设计的,使得它们能够在严格空检查模式和常规类型检查模式下都能够使用。尤其是在常规类型检查模式中,null
和undefined
类型会自动从联合类型中删除(因为它们是其它所有类型的子类型),!
非空断言表达式操作符也被允许使用但是没有任何作用。因此,声明文件使用null和undefined敏感类型更新后,在常规类型模式中仍然是可以向后兼容使用的。
在实际应用中,严格空检查模式要求编译的所有文件都是null和undefined敏感类型。
基于控制流的类型分析
TypeScript 2.0实现了对局部变量和参数的控制流类型分析。以前,对类型保护进行类型分析仅限于if
语句和?:
条件表达式,并且不包括赋值和控制流结构的影响,例如return
和break
语句。使用TypeScript 2.0,类型检查器会分析语句和表达式所有可能的控制流,在任何指定的位置对声明为联合类型的局部变量或参数产生最可能的具体类型(缩小范围的类型)。
示例
function foo(x: string | number | boolean) {
if (typeof x === "string") {
x; // 这里x的类型是string
x = 1;
x; // 这里x的类型是number
}
x; // 这里x的类型是number | boolean
}
function bar(x: string | number) {
if (typeof x === "number") {
return;
}
x; // 这里x的类型是string
}
基于控制流的类型分析在--strictNullChecks
模式中尤为重要,因为可空类型使用联合类型来表示:
function test(x: string | null) {
if (x === null) {
return;
}
x; // 在函数的剩余部分中,x类型是string
}
而且,在--strictNullChecks
模式中,基于控制流的分析包括,对类型不允许为undefined
的局部变量有明确赋值的分析。
function mumble(check: boolean) {
let x: number; // 类型不允许为undefined
x; // 错误,x是undefined
if (check) {
x = 1;
x; // 正确
}
x; // 错误,x可能是undefi
x = 2;
x; // 正确
}
标记联合类型
TypeScript 2.0实现了标记(或区分)联合类型。具体而言,TS编译器现在支持类型保护,基于判别属性的检查来缩小联合类型的范围,并且switch
语句也支持此特性。
示例
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
// 在下面的switch语句中,s的类型在每一个case中都被缩小
// 根据判别属性的值,变量的其它属性不使用类型断言就可以被访问
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius * s.radius;
}
}
function test1(s: Shape) {
if (s.kind === "square") {
s; // Square
}
else {
s; // Rectangle | Circle
}
}
function test2(s: Shape) {
if (s.kind === "square" || s.kind === "rectangle") {
return;
}
s; // Circle
}
判别属性类型保护是x.p == v
、x.p === v
、x.p != v
或者x.p !== v
其中的一种表达式,p
和v
是一个属性和字符串字面量类型或字符串字面量联合类型的表达式。判别属性类型保护缩小x
的类型到由判别属性p
和v
的可能值之一组成的类型。
请注意,我们目前只支持字符串字面值类型的判别属性。我们打算以后添加对布尔值和数字字面量类型的支持。
never
类型
TypeScript 2.0引入了一个新原始类型never
。never
类型表示值的类型从不出现。具体而言,never
是永不返回函数的返回类型,也是变量在类型保护中永不为true的类型。
never
类型具有以下特征:
never
是所有类型的子类型并且可以赋值给所有类型。- 没有类型是
never
的子类型或能赋值给never
(never
类型本身除外)。 - 在函数表达式或箭头函数没有返回类型注解时,如果函数没有
return
语句,或者只有never
类型表达式的return
语句,并且如果函数是不可执行到终点的(例如通过控制流分析决定的),则推断函数的返回类型是never
。 - 在有明确
never
返回类型注解的函数中,所有return
语句(如果有的话)必须有never
类型的表达式并且函数的终点必须是不可执行的。
因为never
是每一个类型的子类型,所以它总是在联合类型中被省略,并且在函数中只要其它类型被返回,类型推断就会忽略never
类型。
一些返回never
函数的示例:
// 函数返回never必须无法执行到终点
function error(message: string): never {
throw new Error(message);
}
// 推断返回类型是never
function fail() {
return error("Something failed");
}
// 函数返回never必须无法执行到终点
function infiniteLoop(): never {
while (true) {
}
}
一些函数返回never
的使用示例:
// 推断返回类型是number
function move1(direction: "up" | "down") {
switch (direction) {
case "up":
return 1;
case "down":
return -1;
}
return error("Should never get here");
}
// 推断返回类型是number
function move2(direction: "up" | "down") {
return direction === "up" ? 1 :
direction === "down" ? -1 :
error("Should never get here");
}
// 推断返回类型是T
function check<T>(x: T | undefined) {
return x || error("Undefined value");
}
因为never
可以赋值给每一个类型,当需要回调函数返回一个更加具体的类型时,函数返回never
类型可以用于检测返回类型是否正确:
function test(cb: () => string) {
let s = cb();
return s;
}
test(() => "hello");
test(() => fail());
test(() => { throw new Error(); })
只读属性和索引签名
属性或索引签名现在可以使用readonly
修饰符声明为只读的。
只读属性可以初始化和在同一个类的构造函数中被赋值,但是在其它情况下对只读属性的赋值是不允许的。
此外,有几种情况下实体隐式只读的:
- 属性声明只使用
get
访问器而没有使用set
访问器被视为只读的。 - 在枚举类型中,枚举成员被视为只读属性。
- 在模块类型中,导出的
const
变量被视为只读属性。 - 在
import
语句中声明的实体被视为只读的。 - 通过ES2015命名空间导入访问的实体被视为只读的(例如,当
foo
当作import * as foo from "foo"
声明时,foo.x
是只读的)。
示例
interface Point {
readonly x: number;
readonly y: number;
}
var p1: Point = { x: 10, y: 20 };
p1.x = 5; // 错误,p1.x是只读的
var p2 = { x: 1, y: 1 };
var p3: Point = p2; // 正确,p2的只读别名
p3.x = 5; // 错误,p3.x是只读的
p2.x = 5; // 正确,但是因为别名使用,同时也改变了p3.x
class Foo {
readonly a = 1;
readonly b: string;
constructor() {
this.b = "hello"; // 在构造函数中允许赋值
}
}
let a: Array<number> = [0, 1, 2, 3, 4];
let b: ReadonlyArray<number> = a;
b[5] = 5; // 错误,元素是只读的
b.push(5); // 错误,没有push方法(因为这会修改数组)
b.length = 3; // 错误,length是只读的
a = b; // 错误,缺少修改数组的方法
指定函数中this
类型
紧跟着类和接口,现在函数和方法也可以声明this
的类型了。
相关阅读 >>
更多相关阅读请进入《typescript》频道 >>

Vue.js 设计与实现 基于Vue.js 3 深入解析Vue.js 设计细节
本书对 Vue.js 3 技术细节的分析非常可靠,对于需要深入理解 Vue.js 3 的用户会有很大的帮助。——尤雨溪,Vue.js作者