JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol。
布尔型
let isDone: boolean = false;
数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
字符串
let myName: string = 'Tom';
let sentence: string = `Hello, my name is ${myName}.`
Null 和 Undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和 void相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。
然而,当你指定了—strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型 string | null | undefined
。
装箱与拆箱
以下,使用 Boolean 进行演示,其他基本类型(除了 null 和 undefined)一样,不再赘述。
使用构造函数 Boolean 创造的对象不是布尔值:
let createdByNewBoolean: boolean = new Boolean(1);
// error TS2322: Type 'Boolean' is not assignable to type 'boolean'.
事实上 new Boolean() 返回的是一个 Boolean 对象:
let createdByNewBoolean: Boolean = new Boolean(1);
直接调用 Boolean 也可以返回一个 boolean 类型:
let createdByBoolean: boolean = Boolean(1);
但是一个原始数据类型可以使用装箱类型声明:
let createdByNewBoolean: Boolean = false;
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
myFavoriteNumber = true;
// error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.
联合类型使用 |
分隔每个类型。
这里的 let myFavoriteNumber: string | number
的含义是,允许 myFavoriteNumber
的类型是 string
或者 number
,但是不能是其他类型。
访问联合类型的属性或方法
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length;
}
//error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
上例中,length
不是 string
和 number
的共有属性,所以会报错。
访问 string
和 number
的共有属性是没问题的:
function getString(something: string | number): string {
return something.toString();
}
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错
// error TS2339: Property 'length' does not exist on type 'number'.
上例中,第二行的 myFavoriteNumber
被推断成了 string
,访问它的 length
属性不会报错。
而第五行的 myFavoriteNumber
被推断成了 number
,访问它的 length
属性时就报错了。
数组
指定一个类型的数组,或通过泛型指定:
let list1: number[] = [1, 2, 3]
let list2: Array<number> = [4, 5, 6]
编译成JS:
var list1 = [1, 2, 3];
var list2 = [4, 5, 6];
只读数组
TypeScript 具有 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
ro = []; // okay
上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。但是可以用类型断言重写:
a = ro as number[];
用接口表示数组
接口也可以用来描述数组:
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
NumberArray
表示:只要 index
的类型是 number
,那么值的类型必须是 number
。
任意类型元素的数组
一个比较常见的做法是,用 any 表示数组中允许出现任意类型:
let list: any[] = ['Xcat Liu', 25, { website: 'https://www.xiaoyulive.top' }];
元组
元组相当于是一个定义了每个下标的元素的类型的数组
let x: [string, boolean] = ['show', false]
编译成JS:
var x = ['show', false];
也可先声明,然后整体赋值或按照索引赋值:
let x: [string, boolean]
x = ['show', false]
x[0] = 'show'
x[1] = false
越界的元素
当试图添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
let x: [string, boolean] = ['show', false]
x.push(true)
x.push('hide')
// [ 'show', false, true, 'hide' ]
枚举
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。
枚举类型支持正向映射和反向映射:
enum Color { Red = 1, Green = 2, Blue = 3 }
console.log(Color.Blue) // 3
console.log(Color[1]) // Red
编译成JS:
var Color;
(function (Color) {
Color[Color["Red"] = 1] = "Red";
Color[Color["Green"] = 2] = "Green";
Color[Color["Blue"] = 3] = "Blue";
})(Color || (Color = {}));
console.log(Color[1]); // Red
console.log(Color.Blue); // 3
手动赋值
我们也可以给枚举项手动赋值:
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增,步长为1。
如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true
上面的例子中,递增到 3
的时候与前面的 Sun
的取值重复了,但是 TypeScript 并没有报错,导致 Days[3]
的值先是 "Sun"
,而后又被 "Wed"
覆盖了。编译的结果是:
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
所以使用的时候需要注意,最好不要出现这种覆盖的情况。
手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):
enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};
var Days;
(function (Days) {
Days[Days["Sun"] = 7] = "Sun";
Days[Days["Mon"] = 8] = "Mon";
Days[Days["Tue"] = 9] = "Tue";
Days[Days["Wed"] = 10] = "Wed";
Days[Days["Thu"] = 11] = "Thu";
Days[Days["Fri"] = 12] = "Fri";
Days[Days["Sat"] = "S"] = "Sat";
})(Days || (Days = {}));
当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1
:
enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1.5); // true
console.log(Days["Tue"] === 2.5); // true
console.log(Days["Sat"] === 6.5); // true
常数项和计算所得项
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
前面我们所举的例子都是常数项,一个典型的计算所得项的例子:
enum Color {Red, Green, Blue = "blue".length};
上面的例子中,"blue".length
就是一个计算所得项。
上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错:
enum Color {Red = "red".length, Green, Blue};
// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.
如果枚举有 const
修饰,也会报错:
const enum Color {Red, Green, Blue = "blue".length};
常数枚举
常数枚举是使用 const enum
定义的枚举类型:
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
上例的编译结果是:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
Any
通过 any 可以指定一个不确定类型的变量
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay
跟普通的JS一样可以动态扩展:
let obj: object = { a: 0, b: 2}
obj.c = 3 // Error: Property 'c' does not exist on type 'object'.
let obj: any = { a: 0, b: 2}
obj.c = 3 // okay
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let something;
something = 'one';
something = 1;
注意,如果声明的时候同时赋值,将触发类型推断:
let something = 'one';
something = 1; // Type '1' is not assignable to type 'string'.ts(2322)
Void
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void {
console.log("This is my warning message");
}
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
let unusable: void = undefined;
Never
never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。
never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。即使 any也不可以赋值给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) {
}
}
Object
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
使用object类型,就可以更好的表示像Object.create这样的API。例如:
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
类型断言
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式。 其一是 尖括号
语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一个为 as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须用 as 语法。
应用: 将一个联合类型的变量指定为一个更加具体的类型
之前提到过,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法, 此时可以使用类型断言,将 something 断言成 string:
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}
类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的:
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
// index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
// Type 'number' is not comparable to type 'boolean'.
类型别名
使用 type 关键字为已有类型取一个别名:
// string
type Name = string;
// function
type NameResolver = () => string;
// 泛型
type Container<T> = { value: T };
也可以使用类型别名来在属性里引用自己,比如二叉树:
type Tree<T> = {
value: T;
left?: Tree<T>;
right?: Tree<T>;
}
let tree: Tree<number> = {
value: 1,
left: {
value: 2
},
right: {
value: 3
}
}
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型,比如链表:
type LinkedList<T> = T & { next?: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person> = {
name: 'xiaoyu',
next: {
name: 'qiaoer'
}
}
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
handleEvent(document.getElementById('hello'), 'scroll'); // 没问题
handleEvent(document.getElementById('world'), 'dbclick'); // 报错,event 不能为 'dbclick'
// error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'.
上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。
注意,类型别名与字符串字面量类型都是使用 type 进行定义。
字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
数字字面量类型
TypeScript还具有数字字面量类型。
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
可辨识联合
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合(Discriminated Unions) 的高级模式,它也称做 标签联合 或 代数数据类型。可辨识联合在函数式编程很有用处。一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。它具有3个要素:
- 具有普通的单例类型属性 — 可辨识的特征。
- 一个类型别名包含了那些类型的联合 — 联合。
- 此属性上的类型保护。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
首先我们声明了将要联合的接口。 每个接口都有 kind
属性但有不同的字符串字面量类型。 kind
属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle;
现在我们使用可辨识联合:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
多态的this类型
多态的 this
类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this
类型:
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public mul(operand: number): this {
this.value *= operand;
return this;
}
public sub(operand: number): this {
this.value -= operand;
return this;
}
public div(operand: number): this {
this.value /= operand;
return this;
}
}
let v = new BasicCalculator(2)
.mul(5)
.add(1)
.sub(2)
.currentValue(); // 9
编译结果(ES5):
"use strict";
var BasicCalculator = /** @class */ (function () {
function BasicCalculator(value) {
if (value === void 0) { value = 0; }
this.value = value;
}
BasicCalculator.prototype.currentValue = function () {
return this.value;
};
BasicCalculator.prototype.add = function (operand) {
this.value += operand;
return this;
};
BasicCalculator.prototype.mul = function (operand) {
this.value *= operand;
return this;
};
BasicCalculator.prototype.sub = function (operand) {
this.value -= operand;
return this;
};
BasicCalculator.prototype.div = function (operand) {
this.value /= operand;
return this;
};
return BasicCalculator;
}());
var v = new BasicCalculator(2)
.mul(5)
.add(1)
.sub(2)
.currentValue(); // 9
编译结果(ES6):
"use strict";
class BasicCalculator {
constructor(value = 0) {
this.value = value;
}
currentValue() {
return this.value;
}
add(operand) {
this.value += operand;
return this;
}
mul(operand) {
this.value *= operand;
return this;
}
sub(operand) {
this.value -= operand;
return this;
}
div(operand) {
this.value /= operand;
return this;
}
}
let v = new BasicCalculator(2)
.mul(5)
.add(1)
.sub(2)
.currentValue(); // 9
由于这个类使用了 this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
let v = new ScientificCalculator(2)
.mul(5)
.sin()
.add(1)
.currentValue();
如果没有 this
类型, ScientificCalculator
就不能够在继承 BasicCalculator
的同时还保持接口的连贯性。mul
将会返回 BasicCalculator
,它并没有 sin
方法。 然而,使用 this
类型, mul
会返回this
,在这里就是 ScientificCalculator
。
索引类型
使用索引类型(Index types),编译器就能够检查使用了动态属性名的代码。
function pluck<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name];
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let str: string = pluck(person, 'name'); // 'Jarid'
let str2: number = pluck(person, 'age'); // 35
keyof T
,索引类型查询操作符。对于任何类型 T,keyof T
的结果为 T上已知的公共属性名的联合。T[K]
, 索引访问操作符。
例如:
let personProps: keyof Person; // 'name' | 'age'
索引类型和字符串索引签名
keyof 和 T[K]与字符串索引签名进行交互。如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型:
interface A<T> {
[key: string]: T;
}
let keys: keyof A<number> = 'abc'; // string
let value: A<number>['foo'] = 123; // number