类型系统
3309字约11分钟
TypeScript
2023-01-30
✨类型声明
类型声明的写法,一律为在标识符后面添加冒号 + 类型
。函数参数和返回值,也是这样来声明类型。
// 变量声明
const name: string = 'Lfangq';
// 函数声明
function add(a: number, b: number): number {
return a + b;
}
✨类型推断
在变量声明时,如果未指定类型,则 TypeScript 会根据变量的初始值进行类型推断。
let foo = 123; // foo 的类型推断为 number
✨特殊类型
✨any
any
类型是 TypeScript 中最灵活的类型,它可以接受任何值。变量类型一旦设为any
,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
let x: any = "hello";
x(1); // 不报错
x.foo = 100; // 不报错
实际开发中,any类型主要适用以下两个场合:
出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为
any
。为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为
any
。
✨unkown
unkown
类型是 any
类型的子类型,它只能赋值给 any
类型和 unkown
类型。
unknown
类型跟 any
类型的不同之处在于,它不能直接使用。主要有以下几个限制:
unknown
类型的变量,不能直接赋值给其他类型的变量(除了any
类型和unknown
类型)。不能直接调用
unknown
类型变量的方法和属性。unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错。
使用unknown
类型变量只能通过“类型缩小”的方式,所谓“类型缩小”,就是缩小unknown
变量的类型范围,确保不会出错。如下:
let a: unknown = 1;
if (typeof a === "number") {
let r = a + 10; // 正确
}
在集合论上,unknown
也可以视为所有其他类型(除了 any
)的全集,所以它和 any
一样,也属于 TypeScript 的顶层类型。
✨never
never
类型是所有类型的子类型,它表示一个永远不存在的值的类型。多用于声明错误的类型。
function f(): never {
throw new Error("Error");
}
TypeScript 有两个“顶层类型”( any
和 unknown
),但是“底层类型”只有 never
唯一一个。
✨基本类型
TypeScript 继承了 JavaScript 的类型设计,以下 8 种类型可以看作 TypeScript 的基本类型,这 8 种基本类型是 TypeScript 类型系统的基础,复杂类型由它们组合而成。
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
注意
注意,上面所有类型的名称都是小写字母,首字母大写的Number
、String
、Boolean
等在 JavaScript 语言中都是内置对象,而不是类型名称。另外,undefined
和 null
既可以作为值,也可以作为类型,取决于在哪里使用它们。
还有 Array
、Set
、Map
、WeakSet
、WeakMap
等都是原生的集合类型,而不是基本类型。
✨boolean 类型
const x: boolean = true;
✨string 类型
const x: string = "hello";
const y: string = `${x} world`;
✨number 类型
number
类型包含所有整数、浮点数和非十进制数。
const x: number = 123; // 整数
const y: number = 3.14; // 浮点数
const z: number = 0xffff; // 非十进制数
✨bigint 类型
bigint
类型包含所有的大整数, 但 bigint
与 number
类型不兼容。
const x: bigint = 123n;
const y: bigint = 0xffffn;
const x: bigint = 123; // 报错
const y: bigint = 3.14; // 报错
注意
注意,bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数target不低于es2020)。
✨symbol 类型
const x: symbol = Symbol();
✨object 类型
根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。
const x: object = { foo: 123 };
const y: object = [1, 2, 3];
const z: object = (n: number) => n + 1;
✨undefined 类型,null 类型
undefined 和 null 是两种独立类型,它们各自都只有一个值。
undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。null 类型也只包含一个值null,表示为空(即此处没有值)。
const x: undefined = undefined;
const y: null = null;
注意
注意,如果没有声明类型的变量,被赋值为undefined
或null
,它们的类型会被推断为any
。如果希望避免这种情况,则需要打开编译选项strictNullChecks
。
undefined
类型,null
类型特殊性:
任何其他类型的变量都可以赋值为 undefined
或 null
。
提示
TypeScript 提供了一个编译选项strictNullChecks
。只要打开这个选项,undefined
和 null
就不能赋值给其他类型的变量(除了any
类型和unknown
类型)。
✨包装对象类型
在原始类型(primitive value)中,只有Symbol()
、BigInt()
、Boolean()
、 String()
、 Number()
有对应的包装对象(wrapper object),其他的都没有。
所谓“包装对象”,指的是这些值在需要时,会自动产生的对象。
"hello".charAt(1); // 'e'
五种包装对象之中,Symbol()
和 BigInt()
不能作为构造函数使用,但是剩下三种 Boolean()
、 String()
、 Number()
可以。
const s = new String("hello");
typeof s; // 'object', 包装对象返回类型是object, 不是string
s.charAt(1); // 'e'
由于包装对象的存在,导致每一个原始类型的值都有包装对象
和字面量
两种情况, 其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。如下
// 包装对象
const s1: String = "hello"; // 正确
const s2: String = new String("hello"); // 正确
// 字面量
const s3: string = "hello"; // 正确
const s4: string = new String("hello"); // 报错
提示
建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。
而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
还有 Symbol()
和 BigInt()
这两个函数不能当作构造函数使用,所以没有办法直接获得 symbol
类型和 bigint
类型的包装对象,因此 Symbol
和 BigInt
这两个类型虽然存在,但是完全没有使用的理由。
✨Object 类型与 object 类型
Object
类型是所有对象的父类型(所有可以转成对象的值,都是Object
类型,这囊括了几乎所有的值, 除了 undefined
和 null
),而 object
类型是所有对象字面量的父类型 (只包含对象、数组和函数,不包括原始类型的值)。
// Object 类型
let obj: Object;
obj = true;
obj = "hi";
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
// undefined 和 null这两个值不能转为对象
obj = undefined; // 报错
obj = null; // 报错
// object 类型
let obj: object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错
变量类型指定为object
在以下几种情况下很有用:
- 通用对象处理:当你编写一个函数,这个函数接受任何类型的对象作为参数,但你不关心这些对象的内部结构时。使用object类型可以确保传入的参数确实是一个对象,而不是其他原始类型(如
string
、number
、boolean
、null
或undefined
)。
function printObjectKeys(obj: object) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key);
}
}
}
避免自动类型推断:TypeScript具有强大的类型推断能力,但有时候你可能想要明确地指定一个变量是
object
类型,以避免TypeScript基于上下文自动推断出更具体的类型。这有助于确保代码的一致性和清晰度。与JavaScript代码交互:当TypeScript代码需要与JavaScript代码交互,且你只知道JavaScript部分返回一个对象,但不确定具体结构时,使用
object
类型可以提供一个基本的类型安全保证。类型兼容性:在某些复杂的类型兼容性检查场景中,使用
object
类型可以帮助你更精确地控制哪些类型被认为是兼容的。函数重载:在定义函数重载时,如果有一个重载版本接受任何对象,而另一个版本接受具有特定属性的对象,那么接受任何对象的版本可能会使用
object
类型。
注意
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型 object
,不使用大写类型 Object
。
注意,无论是大写的 Object
类型,还是小写的 object
类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。
const o1: Object = { foo: 0 };
const o2: object = { foo: 0 };
o1.toString(); // 正确
o1.foo; // 报错
o2.toString(); // 正确
o2.foo; // 报错
✨值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。当 TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型
。
// x 的类型是 "https"
const x = "https";
// y 的类型是 string
const y: string = "https";
// z 的类型是 string
let z = "https";
只包含单个值的值类型
,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。
注意
注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。
// x 的类型是 { foo: number }
const x = { foo: 1 };
✨联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
联合类型 A|B
表示,任何一个类型只要属于 A
或 B
,就属于联合类型 A|B
。
let x: string | number;
x = 123; // 正确
x = "abc"; // 正确
let setting: true | false;
let gender: "male" | "female";
let rainbowColor: "赤" | "橙" | "黄" | "绿" | "青" | "蓝" | "紫";
如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。
“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。
function printId(id: number | string) {
console.log(id.toUpperCase()); // 报错
}
// 类型缩小
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
// “类型缩小”的另一个例子
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
的特征。c
交叉类型的主要用途:
- 对象的合成
let obj: { foo: string } & { bar: string };
obj = {
foo: "hello",
bar: "world",
};
- 为对象类型添加新属性
type A = { foo: number };
type B = A & { bar: number };
✨type 命令
type
命令用来定义一个类型的别名。
type Age = number;
let age: Age = 55;
type
命令特点:
别名不允许重名
别名的作用域是块级作用域
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
// 别名不允许重名
type Color = "red";
type Color = "blue"; // 报错
// 别名的作用域是块级作用域
type Color = "red";
if (Math.random() < 0.5) {
type Color = "blue";
}
// 别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
type World = "world";
type Greeting = `hello ${World}`;
注意
type
命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
✨typeof 运算符
在JavaScript 里面,typeof运算符只可能返回八种结果,而且都是字符串。TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
// 在JavaScript
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n; // "bigint"
// 在TypeScript
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
✨块级类型声明
TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。
if (true) {
type T = number;
let v: T = 5;
} else {
type T = string;
let v: T = "hello";
}
✨类型的兼容
TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。
兼容规则如下:
- 凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行
let a: "hi" = "hi";
let b: string = "hello";
// a 是 b 的子类型,b 是 a 的父类型
b = a; // 正确
a = b; // 报错