TypeScript 的类型系统
基本类型
概述
JavaScript 语言将值分成 8 种类型。
boolean
string
number
undefined
null
symbol
bigint
object (array, function)
TypeScript 继承了这一点,以上 8 种类型可以看作 TypeScript 的基本类型。
注意,上面所有类型的名称都是小写字母,首字母大写的Number、String、Boolean等在 JavaScript 语言中都是内置对象,而不是类型名称。
另外,undefined 和 null 既是值,也是类型,取决于在哪里使用它们。
复杂类型由它们组合而成。
boolean 类型
boolean类型只包含true和false两个布尔值。
string 类型
string类型包含所有字符串。
number 类型
number类型包含所有的数字,整数、浮点数和非十进制数都属于 number 类型。
const x: number = 123;
const y: number = 3.14;
const z: number = 0xffff;bigint 类型
bigint 类型包含所有的大整数。
const x: bigint = 123n;
const y: bigint = 0xffffn;上面示例中,变量x和y就属于 bigint 类型。
bigint 与 number 类型不兼容。
注意,bigint 类型是 ES2020 引入的。如果使用这个类型,TypeScript 编译的 target 不能低于 es2020
symbol 类型
symbol 类型包含所有的 Symbol 值。
object 类型
根据 JavaScript 的设计,对象、数组、函数都属于 object 类型。
undefined 类型,null 类型
undefined和null既是值,又是类型。
注意,如果没有声明类型的变量,被赋值为undefined或null,它们的类型会被推断为any。
let a = undefined; // any
const b = undefined; // any
let c = null; // any
const d = null; // anyTS 提供了一个编译选项strictNullChecks。只要打开这个选项,undefined和null就不能赋值给其他类型的变量(除了any类型和unknown类型),否则,它们可以被赋值给任意类型而不报错
// 打开编译设置 strictNullChecks
let a = undefined; // undefined
const b = undefined; // undefined
let c = null; // null
const d = null; // null这个选项在配置文件tsconfig.json的写法如下。
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}包装对象类型
包装对象
JavaScript 的 8 种类型之中,undefined和null其实是两个特殊值,object属于复合类型,剩下的五种属于原始类型,代表最基本的、不可再分的值。
- boolean
- string
- number
- bigint
- symbol
上面这五种原始类型的值,都有对应的包装对象(wrapper object)。
运行时自动装箱,省去了将原始类型的值手动转成对象实例的麻烦。
五种包装对象之中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即Symbol()和BigInt()不能作为构造函数使用),但是剩下三种可以作为构造函数调用,执行后可以直接获取某个原始类型值的包装对象。
包装对象类型与原始字面量类型
由于包装对象的存在,导致每一个原始类型的值都有包装对象和原始字面量两种情况。
"hello"; // 原始字面量 ‘string’
new String("hello"); // 包装对象 ‘String’上面示例中,第一行是原始字面量,第二行是包装对象,它们都是字符串。
为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
其中,**大写类型同时包含包装对象和原始字面量两种情况,小写类型只包含原始字面量,不包含包装对象。
let s = '123' // string 类型
let s2 = new String("123") // String 类型
let s3:String = '123' // String 类型建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
Object 类型与 object 类型
TypeScript 的对象类型也有大写Object和小写object两种。
Object 类型
大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值,除了 undefined , null
let obj: Object;
obj = undefined // 报错
obj = null // 报错
// 正确
obj = true;
obj = "hi";
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;另外,空对象{}是Object类型的简写形式,所以使用Object时常常用空对象代替。实际上Object都跟 any 差不多了,用的很少。
object 类型
小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数。
let obj: object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错上面示例中,object类型不包含原始类型值,只包含对象、数组和函数。
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object。
字面量类型 Literal Type
TypeScript 规定,单个值也是一种类型,称为“值类型”。
TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
// x 的类型是 "https"
const x = "https";
// y 的类型是 string
const y: string = "https";注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。
// x 的类型是 { foo: number }
const x = { foo: 1 };const x: 5 = 4 + 1; // 报错, 右侧的返回值是 number 类型联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。
联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。
let x: string | number;
x = 123; // 正确
x = "abc"; // 正确类型缩小
如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing) ,区分该值到底属于哪一种类型,然后再进一步处理。
解决方法就是对参数id做一下类型缩小,确定它的类型以后再进行处理。
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。
实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。
下面是“类型缩小”的另一个例子。
function getPort(scheme: "http" | "https") {
switch (scheme) {
case "http":
return 80;
case "https":
return 443;
}
}交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。
交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。
let x: number & string;上面示例中,变量x同时是数值和字符串,这当然是不可能的,所以 TypeScript 会认为x的类型实际是never。
交叉类型的主要用途是对象的合成。
let obj: { foo: string } & { bar: string };
obj = {
foo: "hello",
bar: "world",
};交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };type 命令
type命令用来定义一个类型的别名(Type Aliases)。
type Age = number;
let age: Age = 55;上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。
别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。
type 类型别名有那么几个特点:
- 同作用域下,不允许重名
- 块级作用域
typeof 运算符
JavaScript 语言中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。typeof运算符只可能返回八种结果,而且都是字符串。
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n; // "bigint"TypeScript 中类型运算中(注意不是值运算)也有个typeof运算符,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
function foo<T extends number | string>(a: T): T {
return a
}
let a: number = 1
let b: string = '123'
let ans = foo(a)
let ans2 = foo(b)
console.log(typeof ans) // "number" , js 的 typeof 运算
type myType = typeof ans2 // type myType = string , ts 的 typeof
let c:myType = 'hello'
console.log(typeof c) // "string", js 的 typeof 运算由于编译时不会进行 JavaScript 的值运算,所以 TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式。
type T = typeof Date(); // 报错另外,typeof命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age; // 报错typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo的类型,这时使用typeof foo就可以获得它的类型。
类型的兼容
TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。
TypeScript 为这种情况定义了一个专门术语。如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型(subtype)。类型number就是类型number|string的子类型。
TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。
let a: "hi" = "hi";
let b: string = "hello";
b = a; // 正确
a = b; // 报错