TypeScript 常见类型
1. 基础类型
JS 中将值分为 8 种类型:
- boolean
const x: boolean = true;
const y: boolean = false;
2
- string
const x: string = "hello";
const y: string = `${x} world`;
2
- number
const x: number = 123;
const y: number = 3.14;
const z: number = 0xffff;
2
3
- bigint
const x: bigint = 123n;
const y: bigint = 0xffffn;
2
- symbol
const x: symbol = Symbol();
- null
const x: null = null;
- undefined
let x: undefined = undefined;
- object
const x: object = { foo: 123 };
const y: object = [1, 2, 3];
const z: object = (n: number) => n + 1;
// 还包括 new Date() 等等所有对象...
2
3
4
TS 继承了 JS 全部的功能,所以上面的 8 中类型也是 TS 中的基本类型。
需要主要上面的类型名称都是小驼峰,大驼峰命名的 Number
、String
、Boolean
是 JS 中的内置对象,而不是类型名称。
2. 包装对象的概念
上面的 1-5 类型属于原始类型,undefined 和 null 是两个特殊值,剩下的 object 属于复合类型(复杂类型)。
5 种原始类型都有对应的包装对象,也就是这些值在使用时会自动产生对象的效果,例如:
// 字符串不是对象,但能够像对象一样调用方法
"hello".chartAt(1);
2
原始类型本身没有属性和方法,可以看作在调用时原始类型会自动转换为包装对象,这样的设计方便了原始类型值处理。
5 种包装对象中,symbol 和 bigint 无法直接获取包装对象(即Symbol()
和 BigInit()
不能作为构造函数使用),剩下的String
、Number
、Boolean
都可以当作构造函数使用,通过 new 命令调用得到包装对象,但不带 new 命令,直接当作函数调用时得到的是一个普通的原始类型值。
3. 包装对象类型和字面量类型
由于包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。
"hello"; // 字面量
new String("hello"); // 包装对象
2
为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。
const s1: String = "hello"; // 正确
const s2: String = new String("hello"); // 正确
const s3: string = "hello"; // 正确
const s4: string = new String("hello"); // 报错
2
3
4
5
建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错
目前在 TypeScript 里面,symbol 和 Symbol 两种写法没有差异,bigint 和 BigInt 也是如此,建议始终使用小写的 symbol 和 bigint。
4. Object 与 object 类型
大写的Object
类型代表 JS 中的广义对象。除了 undefined 和 null 这两个值不能转为对象,其他任何值都可以赋值给 Object 类型:
let obj: Object;
obj = true;
obj = "hi";
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = undefined; // 报错
obj = null; // 报错
2
3
4
5
6
7
8
9
10
11
另外,空对象{}
是Object
类型的简写形式,所以使用 Object 时常常用空对象代替。
小写的object
类型代表 JS 中的狭义对象,只包含对象、数组和函数,不包括原始类型的值。
let obj: object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错
2
3
4
5
6
7
8
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型 object,不使用大写类型 Object。
无论是大写的 Object
类型,还是小写的 object
类型,都只包含 JS 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中:
const o1: Object = { foo: 0 };
const o2: object = { foo: 0 };
o1.toString(); // 正确
o1.foo; // 报错
o2.toString(); // 正确
o2.foo; // 报错
2
3
4
5
6
7
8
5. undefined 和 null
undefined
和null
既是值,又是类型。
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为 undefined 或 null。
let age: number = 24;
age = null; // 正确
age = undefined; // 正确
let x: null = null;
x = undefined; // 正确
let y: undefined = undefined;
y = null; // 正确
2
3
4
5
6
7
8
9
JS 中变量如果等于 undefined 就表示还没有赋值,如果等于 null 就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
但实际开发中可能不想要这样的行为发生,所以 TS 提供了一个编译选项 strictNullChecks
。打开这个选项后,undefined 和 null 就不能赋值给其他类型的变量(除了 any 和 unknown 类型),null 和 undefined 也不再能够互相赋值:
// tsc --strictNullChecks app.ts
let age: number = 24;
age = null; // 报错
age = undefined; // 报错
let x: null = null;
x = undefined; // 报错
let y: undefined = undefined;
y = null; // 报错
2
3
4
5
6
7
8
9
10
6. 常值类型
TS 中单个值也是合法的类型,成为“常值类型”:
let x: "hello";
x = "hello"; // 正确
x = "world"; // 报错
2
3
使用常值类型后就只能赋值为定义的值。在用 const
声明的变量没有标注类型时,因为 const 无法修改,TS 就会推断这个变量是常值类型:
const x = "hello"; // 类型是 “hello”
但如果 const 赋值为对象,并不会推断为一个常值类型,而是会放宽内部属性的类型,因为赋值为对象时内部的属性还是可以修改的:
const y = { a: 1 }; // 类型是 { a:number }
值类型可能会出现一些很奇怪的报错。
const x: 5 = 4 + 1; // 报错
上面的 4+1
会被推测为 number
类型,而定义的是常值类型 5
,number 是 5 的父类型,父类型不能赋值给子类型所以报错。但是反过来是可以的:
let x: 5 = 5;
let y: number = x; // 正确
2
3
单个常值类型通常没有意义,常见的使用场景是配合联合类型标识一个值的若干可能:
let gender: "male" | "female";
7. 数组类型
TS 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零个成员。
数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号;第二种是泛型写法,使用内置的 Array
接口:
let a: number[] = [1, 2, 3];
let b: Array<string> = ["a", "b", "c"];
2
3
因为数组成员可以动态变化,所以 TS 不会对数组边界进行检查,越界访问数组并不会报错:
let arr: number[] = [1, 2, 3];
let foo = arr[3]; // 正确
2
如果数组变量没有声明类型,TS 会根据值推断出不同的类型。
如果变量的初始值是空数组,那么 TS 会推断数组类型是any[]
,后面为这个数组赋值时会自动更新类型推断:
const arr = []; // 推断 arr 为 any[]
arr.push(123); // 推断 arr 为 number[]
arr.push("abc"); // 推断 arr 为 (string|number)[]
2
3
4
5
但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新:
// 推断类型为 number[]
const arr = [123];
arr.push("abc"); // 报错
2
3
4
const 声明的数组是可以改变的,但有时候确实有声明制度数组的需求,所以 TS 提供了 readonly
语法和同样的泛型语法 ReadonlyArray
:
const x: readonly number[] = [1, 2, 3];
x.push(4); // 报错
const y: ReadonlyArray<string> = ["a", "b", "c"];
y.pop(); // 报错
2
3
4
5
TS 将 number[]
视为 readonly number[]
的子类型,因为只读数组没有改变原数组的方法,所以number[]
的方法数量多于 readonly number[]
,意味着 number[]
其实是 readonly number[]
的子类型。所以 number[]
类型可以用于所有只读数字数组类型的场合,但返回来不行:
let a1: number[] = [0, 1];
let a2: readonly number[] = a1; // 正确
a1 = a2; // 报错
// 只读数组类型无法代替数组类型
function getSum(s: number[]) {}
getSum(a2); // 报错
2
3
4
5
6
7
8
9
只读数组还有一种声明方法,就是使用const
断言:
const arr = [1, 2, 3] as const; // 类型是:readonly [1, 2, 3]
TS 中使用 T[][]
的形式表示二维数组,多维数组同理。
8. symbol 类型
Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等。
Symbol 值通过Symbol()
函数生成。在 TS 里面,Symbol 的类型使用 symbol 表示(前面提到了使用Symbol
也是一样的效果,但不建议使用)。
let x: symbol = Symbol();
let y: symbol = Symbol();
x === y; // false
2
3
4
上面示例中,变量 x 和 y 的类型都是 symbol
,且都用 Symbol()
生成,但是它们是不相等的。
symbol
类型也满足 symbol 值的特点,表示所有的 Symbol 值,所以无法表示某一个具体的 Symbol 值。为了解决这个问题,TS 设计了一个 symbol 的子类型 unique symbol
。
因为表示单个值,所以只能用 const 声明。const 命令为变量赋值 Symbol 值时,变量类型默认就是 unique symbol
(let 声明变量而不声明类型时会被推断为 symbol),所以类型可以省略不写,但如果赋值为另一个 symbol 类型的变量,还是会被推断为 symbol 类型:
// 正确
// 等同于 const a = Symbol();
const a: unique symbol = Symbol();
// 报错,只能用 const 声明 unique symbol
let b: unique symbol = Symbol();
// let 声明会被自动推断为 symbol 类型
let c = symbol();
let d = Symbol();
// const 声明但赋值一个 symbol类型,推断类型还是 symbol
const e = d;
2
3
4
5
6
7
8
9
10
11
12
13
要想获取同一个 unique symbol
类型,需要使用 typeof value
获取类型:
const a: unique symbol = Symbol();
const b: typeof a = a; // 正确
2
上面的代码将 a 的类型换成 symbol 也不会报错,但是得到的 b 类型还是 symbol,无法表示具体的值。unique symbol 主要的作用就是将变量用作属性名,因为可以表示具体的值所以不会报错,而 symbol 类型无法表示一个具体的值,所以无法用作属性名:
const x: unique symbol = Symbol();
const y: symbol = Symbol();
interface Foo {
[x]: string; // 正确
[y]: string; // 报错
}
2
3
4
5
6
7
unique symbol 类型也可以用作类(class
)的属性值,但只能赋值给类的readonly static
属性。
class C {
static readonly foo: unique symbol = Symbol();
}
2
3
我们知道,相同参数的Symbol.for()
方法会返回相同的 Symbol 值。TS 目前无法识别这种情况,所以可能出现多个 unique symbol 类型的变量,等于同一个 Symbol 值的情况。
// a和b值是相等的,但类型并不相等
const a: unique symbol = Symbol.for("foo");
// 需要使用 const b: typeof a = Symbol.for("foo") 才能让类型也相等
const b: unique symbol = Symbol.for("foo");
2
3
4
unique symbol 作为 symbol 的子类型,所以 unique symbol 可以赋值给 symbol,但反过来不行:
const a: unique symbol = Symbol();
const b: symbol = a; // 正确
const c: unique symbol = b; // 报错
2
3
4
5
9. 函数类型
函数的类型声明,需要给出参数类型和返回值类型,如果不指定参数类型,TS 会推断参数类型,如果缺乏推断的信息,会推断参数类型为 any。参数类型通常可以不写,因为 TS 会根据代码自动推断:
// 如果不声明txt类型,会自动推断为any类型(开启noImplicitAny编译选项时,会报错)
// void 可以不写,会自动推断
function hello(txt: string): void {
console.log("hello" + txt);
}
2
3
4
5
如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
const hello = function (txt: string) {};
// 写法二
// 函数类型中的参数名和实际函数定义的参数名可以不一致
// 函数的实际参数个数,可以少于类型指定参数个数,但不能多于
const hello: (txt: string) => void = function (t) {};
2
3
4
5
6
7
写法二有两个地方需要注意:
- 函数的参数要放在圆括号里面,不放会报错。
- 类型里面的参数名(本例是 txt)是必须的
因为 JS 函数在声明时往往有多余的参数,实际使用时可以只传入一部分参数。因此,TypeScript 允许函数传入的参数不足。
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // 正确
x = y; // 报错
2
3
4
5
TIP
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof
运算符。
function add(x: number, y: number) {
return x + y;
}
const myAdd: typeof add = function (x, y) {
return x + y;
};
2
3
4
5
6
7
函数类型还可以采用对象的写法,但需要注意参数和返回值之间是使用冒号 :
,而不是正常写法的箭头 =>
,这种写法通常只在函数本身存在属性的时候使用。
let add: {
(x: number, y: number): number;
};
add = function (x, y) {
return x + y;
};
2
3
4
5
6
7
TS 中 提供了 Function
类型表示函数,任何函数都属于这个类型,Function 类型相当于:(...args: any[]) => any
如果函数的某个参数可以省略,在参数名后添加 ?
表示。带有问号时,代表该参数实际类型是 原始类型 | undefined
。但是反过来,显式设置了 undefined 类型的参数则不能省略:
function a(x?: number) {}
a(); // 正确
a(undefined); // 正确
a(10); // 正确
function b(x: number | undefined) {}
b(); // 错误
b(undefined); // 正确
b(10); // 正确
2
3
4
5
6
7
8
9
函数的可选参数后不能再有必选参数,这跟 JS 的默认参数语法一致,如果前部分参数有可能为空,只能显式指定参数类型可能为 undefined。
TS 的函数默认值写法于 JS 一致,设置了默认值的参数就是可选的,无需添加 ?
(可选参数语法和默认参数语法同时使用会报错):
function a(x: number, y = 0) {}
// 报错
function b(x?: number = 0) {}
2
3
4
设有默认值的参数如果传入 undefined 也会出发默认值。
函数参数可以加上 readonly
关键字,表示是一个只读参数:
function arraySum(arr: readonly number[]) {
// ...
arr[0] = 0; // 报错
}
2
3
4
有些函数可以接收不同类型或不同个数的参数,并根据参数的不同会有不同的返回值。TS 定义这种函数类型需要使用“函数重载”。TS 对于“函数重载”的声明方法是逐个定义每一种情况的类型,最后合并所有可能的类型:
function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(stringOrArray: string | any[]): string | any[] {
if (typeof stringOrArray === "string")
return stringOrArray.split("").reverse().join("");
else return stringOrArray.slice().reverse();
}
2
3
4
5
6
7
上面示例中,前两行类型声明列举了重载的各种情况。第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容。重载函数内部必须判断参数的类型及个数,并根据判断结果执行不同操作。函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突:
// 报错
function fn(x: boolean): void;
function fn(x: string): void;
function fn(x: number | string) {
console.log(x);
}
2
3
4
5
6
对象的方法也能使用重载:
class StringBuilder {
#data = "";
add(num: number): this;
add(bool: boolean): this;
add(str: string): this;
add(value: any): this {
this.#data += String(value);
return this;
}
toString() {
return this.#data;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
函数重载可以用对象形式表示:
let createElement: {
(tag: "a"): HTMLAnchorElement;
(tag: "canvas"): HTMLCanvasElement;
(tag: "table"): HTMLTableElement;
(tag: string): HTMLElement;
};
2
3
4
5
6
上面声明的 createElement
变量是一个函数,类型为支持上面 3 种情况的重载函数。
JS 中的构造函数特点就是需要用 new
命令生成。在 TS 中声明构造函数的类型也只需要在函数声明前添加 new
关键字:
let a: new () => string;
let b: {
new (): string;
};
2
3
4
5
10. 对象类型
对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型:
const obj: {
x: number;
y: number;
} = { x: 1, y: 1 };
2
3
4
对象类型属性可以用分号
;
分隔,也可以用逗号,
分隔,最后一个属性后的分隔符可以省略。
上面声明的 obj 类型,准确的定义了 obj 的“形状”,多一个属性或少一个属性都不行,同样的也不能删除存在的属性:
obj.z; // 读取不存在的属性,报错
obk.z = 1; // 增加不存在的属性,报错
delete obj.x; // 删除存在的属性,报错
obj.x = 2; // 修改存在的属性,不报错
2
3
4
对象中的方法使用函数类型描述:
const obj: {
x: number;
y: number;
add(x: number, y: number): number;
// 或者写成
// add: (x:number, y:number) => number;
} = {
x: 1,
y: 1,
add(x, y) {
return x + y;
},
};
2
3
4
5
6
7
8
9
10
11
12
13
如果某个属性是可选的,同样可以在属性名后添加 ?
实现,可选属性等同于允许赋值为undefined
:
const a: {
x: number;
y?: number;
} = { x: 1 };
// 等同于
const b: {
x: number;
y: number | undefined;
} = { x: 1, y: undefined };
2
3
4
5
6
7
8
9
TS 中提供了 ExactOptionalPropertyTypes
编译选项,只要同时开启这个选项和 strictNullChecks
后,可选属性就不能设为 undefined
。需要注意,可选属性与允许设为 undefined 的必选属性是不等价的:
type A = { x: number; y?: number };
type B = { x: number; y: number | undefined };
const ObjA: A = { x: 1 }; // 正确
const ObjB: B = { x: 1 }; // 报错
2
3
4
5
属性名前添加 readonly
关键字表示这个属性是只读属性,不能修改。但是如果属性值是一个对象,readonly
修饰符并不禁止修改对象中的属性,只是禁止替换这个对象:
const person: {
readonly age: number;
readonly info: { name: string };
} = { age: 20, info: { name: "x" } };
person.age = 21; // 报错
person.info.name = "y"; // 正确
2
3
4
5
6
7
另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量:
let w: {
name: string;
age: number;
} = {
name: "Vicky",
age: 42,
};
let r: {
readonly name: string;
readonly age: number;
} = w;
w.age += 1;
r.age; // 43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果希望属性值是只读的,除了声明时加上readonly
关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言as const
。
// 类型为:{ readonly name: "Sabrina"; }
const myUser = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 报错
2
3
4
5
6
如果无法实现知道对象有多少属性,或者不想一个个定义属性类型,可以使用索引类型来描述类型。索引类型里面,最常见的就是属性名的字符串索引:
const obj: {
[property: string]: string;
} = {
foo: "a",
bar: "b",
baz: "c",
};
2
3
4
5
6
7
[property: string]
的property
表示属性名,可以随便起,它的类型是 string。所以只要满足属性名是字符串,属性值也是字符串的属性,不管定义多少个都是正确的。
对象属性名的类型除了 string 类型,还有可能是 number 和 symbol。对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名:
let MyType: {
[x: number]: boolean; // 报错
[x: string]: string;
};
2
3
4
同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。
let MyType: {
foo: boolean; // 报错
[x: string]: string;
};
2
3
4
属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length
属性,因为类型里面没有定义这些东西。
const arr: {
[n: number]: number;
} = [1, 2, 3];
arr.length; // 报错
2
3
4
对象解构赋值的类型声明语法跟对象声明类型一样,但没法直接为单个解构变量指定类型,因为解构语法中的冒号已经作为了重命名的作用,所以必须定义整个结构对象的类型:
// 指定x是string,y是number,从obj中解构出x、y后重命名为foo、bar
let { x: foo, y: bar }: { x: string; y: number } = obj;
2
TS 中只要对象 B 满足 对象 A 的结构特征,就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。
type A = {
x: number;
};
type B = {
x: number;
y: number;
};
2
3
4
5
6
7
8
上面示例中,B 满足 A 的所有属性特征,因此兼容对象 A。所以可以使用 A 的地方,可以使用 B。TS 这样设计是因为 JS 并不关心对象是否严格,只要拥有满足要求的属性就可以正确运行。
如果类型 B 可以赋值给类型 A,TS 就认为 B 是 A 的子类型(subtyping),A 是 B 的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。
这种设计有时会导致令人惊讶的结果。
function getSum(obj: { x: number; y: number }) {
let sum = 0;
for (const n of Object.keys(obj)) {
const v = obj[n]; // 报错,属性不止有x、y时也能传入
sum += Math.abs(v);
}
return sum;
}
2
3
4
5
6
7
8
9
10
编译器选项中的 suppressExcessPropertyErrors
,开启后可以关闭多余属性检查。
如果对象使用字面量表示(直接赋值),会触发 TS 的“严格字面量检查”(strict object literal checking),此时有未定义的属性就会报错;但是如果使用变量赋值,救出根据“结构类型”原则,是不会报错的:
const point: {
x: number;
y: number;
} = {
x: 1,
y: 1,
z: 1, // 报错
};
const myPoint = {
x: 1,
y: 1,
z: 1,
};
const point: {
x: number;
y: number;
} = myPoint; // 正确
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。
根据“结构类型”原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。为了避免这种情况,TS 2.4 引入了一个“最小可选属性规则”,也称为“弱类型检测”(weak type detection):
type Options = {
a?: number;
b?: number;
c?: number;
};
const opts = { d: 123 };
const obj: Options = opts; // 报错
2
3
4
5
6
7
8
9
报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。
前文提到了空对象类型 {}
等同于类型 Object
,自身可以赋值除了 null 和 undefined 之外的所有值,但是空对象本身没有自定义属性,所以对空对象的属性赋值就会报错。空对象只能使用继承的原型 Object.prototype
中的属性:
// 被推断为 {} 类型
let obj = {};
obj.prop = 1; // 报错
obj.toString(); // 正确
obj = 1; // 正确
2
3
4
5
也因为 {}
类型可以接收各种类型的值,所以不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性:
const b: {} = { myProp: 1, anotherProp: 2 }; // 正确
b.myProp; // 报错
2