阮一峰《TypeScript 教程》
本篇文章主要记录浏览阮一峰《TypeScript 教程》书籍的过程中本人不会的一些TypeScript的用法。当然,可以说,我都不会哈哈哈,不过有的用法比较奇葩的我就不记录了,只记录我觉得项目中会用到,比较有实用价值的知识点。不得不说,阮老师写的真的是太好了,清晰易懂,而且特别详细。
typescript 在线编译的网站:
https://www.typescriptlang.org/play
类型系统
any、undefined、null
首字母大写的Number
、String
、Boolean
等在 JavaScript 语言中都是内置对象,而不是类型名称。
undefined 和 null 既可以作为值,也可以作为类型
如果没有声明类型的变量,被赋值为undefined
或null
,它们的类型会被推断为any
let a = undefined; // any
const b = undefined; // any
let c = null; // any
const d = null; // any
任何其他类型的变量都可以赋值为undefined
或null
。这并不是因为undefined
和null
包含在number
类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefined
和null
,以便跟 JavaScript 的行为保持一致。
let age:number = 24;
age = null; // 正确
age = undefined; // 正确
但是这种情况,在编译阶段不报错,在运行阶段可能会报错,例如
const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错
为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项strictNullChecks
。只要打开这个选项,undefined
和null
就不能赋值给其他类型的变量(除了any
类型和unknown
类型)。
这个选项在配置文件tsconfig.json
的写法如下:
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
Object与object类型
大写的 Object
类型代表 Javascript 里面的广义对象,所有可以转义成对象的数据,都属于 Object
类型,这囊括了几乎所有的值。
let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
以上都是正确的赋值。事实上,除了 null
和 undefined
,其他的数据都可以赋值给 Object
类型。
另外,空对象 {}
是 Object
的简写形式,所以 Object
常常使用 {}
代替
let obj:{};
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
无所不包的 Object
对象既不符合直觉,也不利于使用。所以尽量不要用…
小写的 object
类型指的是狭义的对象,即可以用字面量表示的对象,包含数组、对象、函数,不包含原始值
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。下面示例,变量x
的类型是字符串hello
,导致它只能赋值为这个字符串,赋值为其他字符串就会报错
let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错
TypeScript 推断类型时,遇到const
命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';
联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
联合类型A|B
表示,任何一个类型只要属于A
或B
,就属于联合类型A|B
。
let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确
上面示例中,变量x
就是联合类型string|number
,表示它的值既可以是字符串,也可以是数值。
如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。
function printId(
id:number|string
) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。
交叉类型
交叉类型A&B
表示,任何一个类型必须同时属于A
和B
,才属于交叉类型A&B
,即交叉类型同时满足A
和B
的特征。交叉类型的主要用途是表示对象的合成。
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
上面示例中,变量obj
同时具有属性foo
和属性bar
。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };
type 命令
type
命令用来定义一个类型的别名。
type Age = number;
let age:Age = 55;
上面示例中,type
命令为number
类型定义了一个别名Age
。这样就能像使用number
一样,使用Age
作为类型。别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。
别名不允许重名。
别名的作用域是块级作用域
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
type World = "world";
type Greeting = `hello ${World}`;
type
命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
typeof 运算符
typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型
JavaScript 里面,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 类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式,typeof
命令的参数不能是类型
块级类型声明
TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。
if (true) {
type T = number;
let v:T = 5;
} else {
type T = string;
let v:T = 'hello';
}
类型的兼容
TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。如果类型A
的值可以赋值给类型B
,那么类型A
就称为类型B
的子类型(subtype)。类型number
就是类型number|string
的子类型。
TypeScript 的数组类型
JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。
数组特征:元素的类型相同,但是数量不定
数组有两种写法
let arr:number[] = [1, 2, 3];
数组arr
的类型是number[]
,其中number
表示数组成员类型是number
如果数组成员的类型比较复杂,可以写在圆括号里面。
let arr:(number|string)[];
数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。
let arr:Array<number> = [1, 2, 3];
let arr:Array<number|string>;
用尖括号的这种写法本质上属于泛型。
数组的类型推断
如果数组初始的值是空的,那么后续赋值过程会根据数组中元素的值自动推断类型,并且会自动更新类型推断
// 推断为 any[]
const arr = [];
// 推断类型为 number[]
arr.push(123);
// 推断类型为 (string | number)[]
arr.push('abc');
但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错
只读数组
TypeScript 允许声明只读数组,即不允许变动数组成员,方法是在数组类型前面加上readonly
关键字。
const arr:readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
TypeScript 将readonly number[]
与number[]
视为两种不一样的类型,后者是前者的子类型。
这是因为只读数组没有pop()
、push()
之类会改变原数组的方法,所以number[]
的方法数量要多于readonly number[]
,这意味着number[]
其实是readonly number[]
的子类型。
readonly
关键字不能与数组的泛型写法一起使用。
// 报错
const arr:readonly Array<number> = [0, 1];
实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];
只读数组还有一种声明方法,就是使用“const 断言”。
const arr = [0, 1] as const;
arr[0] = [2]; // 报错
上面示例中,as const
告诉 TypeScript,推断类型时要把变量arr
推断为只读数组,从而使得数组成员无法改变。
多维数组
TypeScript 使用T[][]
的形式,表示二维数组,T
是最底层数组成员的类型。
var multi:number[][] =[[1,2,3], [23,24,25]];
表示 multi
是一个二维数组,元素的数据类型为 number
TypeScript 的元组类型
元祖是 TypeScript 特有的数据类型,表示元素的数据类型可以不同的数组。
元祖必须声明每个成员的类型
const s:[string, string, boolean] = ['a', 'b', true];
元祖和数组的区分方式就是:元祖的成员类型在方括号里面,数组的成员类型在方括号外面。
使用元祖时,必须设置成员类型,否则就会被推断为一个数组,例如下面的写法:
// a 的类型为 (number | boolean)[]
let a = [1, true];
会被自动推断为成员类型为 (number | boolean)
的数组。
元祖还可以在结尾的成员类型后面加上问号,表示这个成员可有可无
type myTuple = [
number,
number,
number?,
string?
];
上面这个元祖,表示最后两个成员是可选的。
由于需要声明每个成员的类型,元祖在大多数情况下成员数量都很少。元祖的成员数量在类型声明的时候就能看出来,超过声明的数量的话就会报错。
但是,元祖的成员类型声明可以使用 … 扩展运算符来声明不限制成员数量的元祖
type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
上面这个元祖,第一个元素必须是字符串,后面可以有若干个数字类型的成员。
扩展运算符在元素的任意位置都可以,但它的后面只能跟数组或元祖
type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];
只读元祖
元祖也可以是只读的。只读元祖有下面两种写法:
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>
两种写法都可以得到只读元祖,其中写法二是泛型的写法,用到了工具类型 Readonly<T>
跟数组一样,只读元祖是元祖的父类型,因为元祖比只读元祖拥有更多的方法。因此,元祖可以替代只读元祖,只读元祖不能替代元祖。
type t1 = readonly [number, number];
type t2 = [number, number];
let x:t2 = [1, 2];
let y:t1 = x; // 正确
x = y; // 报错
上面代码中,t1 是只读元祖,t2 是普通元祖。可以把普通元祖赋值给只读元祖,但不可以把只读元祖赋值给普通元祖。
成员数量的推断
如果没有扩展运算符,只有普通成员和可选成员,元祖可以推断出成员的数量。
看以下两段代码:
function f(point: [number, number]) {
if (point.length === 3) { // 报错
// ...
}
}
function f(
point:[number, number?, number?]
) {
if (point.length === 4) { // 报错
// ...
}
}
上述两段代码,由于元祖会自动推断成员数量,判断出数量不可能等于进行判断的数值,代码会直接报错。
如果使用了扩展运算符,元祖就无法推断成员数量了。
const myTuple:[...string[]]
= ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
// ...
}
如果使用了扩展运算符,其实是会被当成数组看待,而数组的成员数量是不定的,所以此时无法推断成员数量。
一旦扩展运算符使元祖的成员数量无法确定时,typescript 会把元祖当成数组看待。
扩展运算符与成员数量
扩展运算符会将数组转换成一个用逗号分割的序列,由于数组的成员数量是不定的,使用扩展运算符展开数组的时候,展开的结果的成员数量也是不定的。
这就会导致在函数调用时,如果函数接收的参数数量是一定的,但是传参的时候使用了扩展运算符,在编译的时候就会报错
const arr = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 报错
上述报错,原因是因为 add()
函数只能接受两个参数,而使用扩展运算符传参,typescript 会认为参数数量不确定,因此就报错了。
有些函数是可以接受任意数量的参数的,这种就不会报错,比如 console.log()
解决这个问题有两个办法
- 把成员数量不定的数组,写成成员数量确定的元祖
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 正确
- 使用
as const
断言
const arr = [1, 2] as const;
使用这种写法,arr 的类型是 readonly [1,2]
只读类型,可以当做数组,也可以当做元祖。