基本数据类型
number
,string,boolean,symbol、bigint、object,null或undefined
基本类型
any 类型
any 类型可以赋值任何类型,同样 any 类型可以赋值给其他类型造成污染
unkonwn 为避免any类型污染问题,unknown类型作用与any一致,但是它只能赋值到unknown/any类型的变量上
要想使用 unknown 类型,必须缩小范围,否则无法使用
uknouwn运算有限, 只能进行比较运算(运算符
==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种1
2
3
4
5let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}
1
2
3
4
5let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}never
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为
never,即不可能有这样的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
171、函数抛出错误,不可能有返回值
function f():never {
throw new Error('Error');
}
2、剩余的情况就属于never类型
function fn(x:string|number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}
3、变量x的类型是never,就不可能赋给它任何值,否则都会报错。
let x:never;object 类型
大写的
Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。除了 Null / undefined1
2
3
4
5
6
7
8let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;undefined/null
既是值,又是类型。
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为
undefined或null。
1  | const str: string = 'hello there'  | 
类型注解
1  | # 声明变量类型  | 
类型推断
简单类型不需要写类型注释, 不能自动推断时需要类型注释
当自动类型无法推断出来时,会默认为 any 类型
1  | let one = 1;  | 
值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
1  | let x:'hello';  | 
上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。
TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。(如果赋值为对象,并不会推断为值类型。)
1  | // x 的类型是 "https"  | 
父类型不能赋值给子类型
5 是 number 子类型,4+1 是 number 类型,赋值错误
1
const x:5 = 4 + 1; // 报错
反之则可以
as 断言,将 4+1 断言为 5 类型。
1
const x:5 = (4 + 1) as 5; // 正确
type 命令
type命令用来定义一个类型的别名。
1  | type Age = number;  | 
上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。
交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。
交叉类型常常用来为对象类型添加新属性。
1  | type A = { foo: number };  | 
上面示例中,类型B是一个交叉类型,用来在A的基础上增加了属性bar。
联合类型
1  | let age: string|number = 15  | 
数组
如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是
any[]1
2// 推断为 any[]
const arr = [];然后随着新成员的加入,TypeScript 会自动修改推断的数组类型。
类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
只读
1
2const arr:readonly number = [];
const arr:ReadonlyArray<number> = [];多维数组
T[][]的形式,表示二维数组,T是最底层数组成员的类型1
var multi:number[][] = [[1,2,3], [23,24,25]];
1  | // 数组, 数组中元素是number类型的数组  | 
元组
- 数组的成员类型写在方括号外面(
number[]),元组的成员类型是写在方括号里面([number]) 
1  | // 元组类型顺序数量都要保持一致  | 
- 元组成员的类型可以添加问号后缀(
?),表示该成员是可选的。 
1  | let a:[number, number?] = [1];  | 
上面示例中,元组a的第二个成员是可选的,可以省略。
- 使用扩展运算符(
...),可以表示不限成员数量的元组 
1  | type NamedNums = [  | 
- 如果不确定元组成员的类型和数量,可以写成下面这样。
 
1  | type Tuple = [...any[]];  | 
- 只读元组
 
1  | // 写法一  | 
- 元组形式传参
 
如果arr是数组,数组的数量不确定,就导致报错
1  | const arr:[number, number] = [1, 2];  | 
symbol
1  | let x:symbol = Symbol();  | 
- unique symbol
 
unique symbol 使用 const 定义,使用let定义
1  | const x:unique symbol = Symbol();  | 
枚举
用来将相关常量放在一个容器里面,方便使用。
Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2……
成员的值可以是任意数值,但不能是大整数(Bigint)
1  | // 默认值是下标数字  | 
很大程度上,Enum 结构可以被对象的as const断言替代。
1  | enum Foo {  | 
多个同名的 Enum 结构会自动合并。
1  | enum Foo {  | 
同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。
Enum 成员的值除了设为数值,还可以设为字符串。
1  | enum Direction {  | 
注意,字符串 Enum 的成员值,不能使用表达式赋值。
1  | enum MyEnum {  | 
keyof 运算符
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
1  | enum MyEnum {  | 
上面示例中,keyof typeof MyEnum可以取出MyEnum的所有成员名,所以类型Foo等同于联合类型'A'|'B'。
如果要返回 Enum 所有的成员值,可以使用in运算符。
1  | enum MyEnum {  | 
反向映射
1  | enum MyEnum {  | 
注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。
1  | MyEnum.A // 'a'  | 
对象
声明方式就是使用大括号,并在內部声明属性的方法和类型
1  | // 属性类型以分号结尾  | 
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
除了**type命令可以为对象类型声明一个别名,TypeScript 还提供了interface命令**,可以把对象类型提炼为一个接口。
1  | interface MyObj {  | 
可选属性
如果某个属性时可选的,需要在后面加上一个❓
1  | type User = {  | 
只读属性
在声明属性前面加上 readonly
1  | interface MyInterface {  | 
prop 属性只能在初始化期间赋值,后面再修改就会报错。
还有一种方法,就是在赋值时,在对象后面加上只读断言as const。
1  | const myUser = {  | 
如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。
1  | const myUser:{ name: string } = {  | 
属性名的索引类型
如果对象的属性非常多,一个个声明类型就很麻烦。下面写法可以随意属性取名
1  | type MyObj = {  | 
建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及
length属性,因为类型里面没有定义这些东西。
解构赋值
解构赋值类型声明写在另一个:{} 里,因为原来{}里的:被js语法占用了
1  | const {id, name, price}:{  | 
结构类型原则
只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。
1  | type A = {  | 
上面示例中,对象A只有一个属性x,类型为number。对象B满足这个特征,因此**兼容对象A,只要可以使用A的地方,就可以使用B**。
严格字面量检查
 如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。
如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的
1  | const myPoint = {  | 
如果你确认字面量没有错误,也可以使用类型断言规避严格字面量检查。又或者说如果允许字面量有多余属性,可以像下面这样在类型里面定义一个通用属性。
1  | //1、 断言  | 
最小可选属性
 如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。为了避免这种情况,TypeScript 2.4 引入了一个“最小可选属性规则”,也称为“弱类型检测”(weak type detection)。
 如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。
1  | type Options = {  | 
空对象
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
1  | const obj = {};  | 
空对象作为类型,其实是Object类型的简写形式。
1  | let d:{};  | 
各种类型的值(除了null和undefined)都可以赋值给空对象类型,跟Object类型的行为是一样的。
因为Object可以接受各种类型的值,而空对象是Object类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。
1  | interface Empty { }  | 
上面示例中,变量b的类型是空对象,视同Object类型,不会有严格字面量检查,但是读取多余的属性会报错。
如果想强制使用没有任何属性的对象,可以采用下面的写法。
1  | interface WithoutProperties {  | 
接口
用来描述对象形状的 interface,值必须是对象.对象的模板
1  | // 定义  | 
接口对象属性
接口对象的属性与对象一样,属性索引共有string、number和symbol三种类型。
最多只能定义一个字符串索引, 并且其它指定的属性值也要遵守规则
1  | interface MyObj {  | 
属性的数值索引,其实是指定数组的类型。
1  | interface A {  | 
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。
1  | interface A {  | 
对象的方法
1  | // 写法一  | 
属性名表达式写法
1  | const f = 'f';  | 
方法的重载
1  | interface A {  | 
接口除了在对象上使用,interface 也可以用来声明独立的函数。
1  | interface Add {  | 
构造函数中也可以使用,详情见 classs 类章节
1  | interface ErrorConstructor {  | 
接口的继承
interface 可以继承其他类型,主要有下面几种情况。
- interface 继承 interface
 
  interface 可以使用extends关键字,继承其他 interface。
1  | interface Shape {  | 
注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错,多重继承时也是如此。重名时子属性会覆盖父属性
interface 继承 type
1
2
3
4
5
6
7
8type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}注意,如果
type命令定义的类型不是对象,interface 就无法继承interface 继承 class
interface 还可以继承 class,即继承该类的所有成员
1
2
3
4
5
6
7
8
9
10
11class A {
x:string = '';
y():boolean {
return true;
}
}
interface B extends A {
z: number
}实现
B接口的对象就需要实现这些属性。1
2
3
4
5const b:B = {
x: '',
y: function(){ return true },
z: 123
}某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class A {
private x: string = '';
protected y: string = '';
}
interface B extends A {
z: number
}
// 报错
const b:B = { /* ... */ }
// 报错
class C implements B {
// ...
}上面示例中,
A有私有成员和保护成员,B继承了A,但无法用于对象,因为对象不能实现这些成员。这导致B只能用于其他 class,而这时其他 class 与A之间不构成父类和子类的关系,使得x与y无法部署。
接口合并
多个同名接口会合并成一个接口。
1  | interface Box {  | 
这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。
示例 如下:扩展window Document 对象
1  | interface Document {  | 
同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。与继承情况类似
关于接口中同名方法,则会发生重载
1  | interface Cloner {  | 
 同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。
 这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
1  | interface A {  | 
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
1  | interface Circle {  | 
上面示例中,接口Circle和Rectangle组成一个联合类型Circle | Rectangle。因此,这个联合类型的同名属性area,也是一个联合类型。
interface 与 type 的异同
 很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
区别
(1)、**type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等**
(2)、interface可以继承其他类型,type 不支持继承。
继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。
1  | type Animal = {  | 
type 和 interface 可以相互继承
interface 继承 type
1  | type Foo = { x: number; };  | 
type 也可以继承 interface
1  | interface Foo {  | 
(3)、同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。
1  | type A = { foo:number }; // 报错  | 
作为比较,interface则会自动合并。
1  | interface A { foo:number };  | 
(4)、**interface不能包含属性映射(mapping),type可以**
1  | interface Point {  | 
(5)、this关键字只能用于interface
1  | // 正确  | 
(6)、type 可以扩展原始数据类型,interface 不行。
1  | // 正确  | 
上面示例中,type 可以扩展原始数据类型 string,interface 就不行。
(7)interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。
1  | type A = { /* ... */ };  | 
上面示例中,类型AorB是一个联合类型,AorBwithName则是为AorB添加一个属性。这两种运算,interface都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用
type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。
接口拓展
1  | // 拓展继承  | 
类型断言
表示这个对象就是这样一个类型
1  | // ISchool 接口没有拓展时,新增 lessons 属性会报错, 这时可以使用断言 人工判断他就是 ISchool 类型  | 
类型守护
通过类型判断返回正确的类型, 可以使用断言,in , instanceof,属性访问等方式
1  | function addObj (first: object | NumberObj, second: object | Number) {  | 
函数
函数主要关系其参数与返回值
函数类型声明
1  | // 基本写法, 没有返回值 声明返回值 void  | 
类型复用
1  | // 箭头函数写法  | 
对象写法
1  | // 对象写法  | 
function 类型
Function 类型的函数可以接受任意数量的参数,每个参数的类型都是any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。
1  | function doSomething(f:Function) {  | 
箭头函数
1  | // 1、箭头函数写法1  | 
map()方法的参数是一个箭头函数(name):Person => ({name})
1  | type Person = { name: string };  | 
此时name的类型省略了,应为可以通过 Person中推断出来
函数的可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示
此时效果相当于 x:原始类型|undefined,但是把这种写法当作可选参数来用就不行了
1  | function f(x?:number) {  | 
函数的可选参数只能在参数列表的尾部,跟在必选参数的后面
1  | et myFunc:  | 
参数解构
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。
1  | 
  | 
rest
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)
1  | // rest 参数为数组  | 
只读参数
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly关键字,表示这是只读参数。
1  | function arraySum(  | 
void 类型
void 返回值类型允许返回 undefined / null
局部类型
局部类型只能在函数内使用, 函数外使用会报错
1  | function hello(txt:string) {  | 
高阶函数
一个函数返回值还是一个函数,我们称这个函数为高阶函数
1  | (someValue: number) => (multiplier: number) => someValue * multiplier;  | 
函数重载
一个函数接收不同类型参数,并根据参数类型不同会有不同函数行为。执行不同逻辑行为,称函数重载
1  | function reverse(str:string):string;  | 
对象的方法也可以使用重载。
1  | class StringBuilder {  | 
构造函数
构造函数的最大特点就是必须使用new 命令调用
1  | class Animal {  | 
泛型
有些时候,函数返回值的类型与参数类型是相关的。
1  | // js  | 
泛型的写法
泛型主要用在四个场合:函数、接口、类和别名。
函数中使用泛型
普通函数
1  | function id<T>(arg:T):T {  | 
变量函数
1  | // 写法一  | 
接口中使用泛型
1  | interface Box<Type> {  | 
定义泛型接口,然后使用
写法一
1  | interface Comparator<T> {  | 
写法二
1  | interface Fn {  | 
类中使用泛型
1  | class SelectGirl<T> {  | 
1  | class A<T> {  | 
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
类型别名的泛型写法
1  | type Container<T> = { value: T };  | 
下面是定义树形结构的例子。
1  | type Tree<T> = {  | 
类型别名Tree内部递归引用了Tree自身。
类型参数默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。
1  | function getFirst<T = string>(  | 
一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。
1  | <T = boolean, U> // 错误  | 
数组的泛型表示
数组类型有一种表示方法是Array<T>。这就是泛型的写法
1  | let arr:Array<number> = [1, 2, 3];  | 
同样的,如果数组成员都是字符串,那么类型就写成Array<string>。事实上,在 TypeScript 内部,数组类型的另一种写法number[]、string[],只是Array<number>、Array<string>的简写形式。
其他的 TypeScript 内部数据结构,比如Map、Set和Promise,其实也是泛型接口,完整的写法是Map<K, V>、Set<T>和Promise<T>。
TypeScript 默认还提供一个**ReadonlyArray<T>接口,表示只读数组。**
泛型约束
很多类型参数并不是无限制的,对于传入的类型存在约束条件。
1  | interface Girl {  | 
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
1  | type Fn<A extends string, B extends string = 'world'>  | 
多个泛型, 元组交换
1  | // 原, 返回值 any  | 
泛型有一些使用注意点。
(1)尽量少用泛型。
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
(2)类型参数越少越好。
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
(3)类型参数需要出现两次。
如果类型参数在定义后只出现一次,那么很可能是不必要的。
(4)泛型可以嵌套。
类型参数可以是另一个泛型。
类
大部分跟js原生类差不多,但是也加入了一些新的东西
顶层属性声明如果不写类型,会被推断为any。如果赋值就可以自动推断类型
如果ts配置了 strictPropertyInitialization, 不赋值初始值就报错,可以这样写
1
2
3
4class Point {
x!: number;
y!: number;
}
1  | class Teacher {  | 
函数的重载
 另外,构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
1  | class Point {  | 
存取器方法
(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
1  | class C {  | 
(2)TypeScript 5.1 版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。
1  | class C {  | 
(3)get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
属性索引
类允许定义属性索引。
1  | class MyClass {  | 
在类中,索性也覆盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。否则会报错
1  | class MyClass {  | 
类的接口
implements 关键字
1  | interface Country {  | 
上面示例中,interface或type都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。
1  | interface A {  | 
 interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
1  | class Car {  | 
implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
实现多个接口
类的继承,继承一个类,这个类实现多个接口
1
2
3
4
5class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable {
}接口继承,实现一个接口,这个接口继承多个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface MotorVehicle {
// ...
}
interface Flyable {
// ...
}
interface Swimmable {
// ...
}
interface SuperCar extends MotoVehicle,Flyable, Swimmable {
// ...
}
class SecretCar implements SuperCar {
// ...
}
注意,发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。
类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
1  | class A {  | 
上面示例中,类A与接口A同名,后者会被合并进前者的类型定义
Class 类型
实例类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
1  | class Color {  | 
当类继承接口时,实例类型可以是 类|接口
1  | interface MotorVehicle {  | 
类自身的类型
要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。
1  | class Point {  | 
结构类型原则
 Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
1  | class Foo {  | 
对象bar满足类Foo的实例结构,只是多了一个属性amount。所以,它可以当作参数,传入函数fn()。
 总之,只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型。不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。
1  | class Person {  | 
这种情况,运算符instanceof不适用于判断某个对象是否跟某个 class 属于同一类型。
1  | obj instanceof Person // false  | 
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。
1  | class Empty {}  | 
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
1  | class Point {  | 
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
1  | // 情况一  | 
上面示例中,A和B都有私有成员(或保护成员)name,这时只有在B继承A的情况下(class B extends A),B才兼容A。
类的继承
 类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
1  | class A {  | 
基类(父类)也可以做为 子类的类型
1  | const a:A = b;  | 
子类可以覆盖基类的同名方法。
1  | class B extends A {  | 
类属性未声明初始值,declare 修饰符
1  | interface Animal {  | 
可访问修饰符
public、private和protected。
这三个修饰符的位置,都写在属性或方法的最前面。
public
表示这是公开成员,外部可以自由访问。public 修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。
protected
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。private
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
注意,子类不能定义父类私有成员的同名成员。
实例属性简写形式
1  | class A {  | 
静态成员
类的内部可以使用static关键字,定义静态成员。
静态成员是只能通过类本身使用的成员。
1  | class MyClass {  | 
静态私有属性也可以用 ES6 语法的#前缀表示,public和protected的静态成员可以被继承。而私有属性成员不会
1  | class MyClass {  | 
泛型类
类也可以写成泛型,使用类型参数。
1  | class Box<Type> {  | 
注意,静态成员不能使用泛型的类型参数。
抽象类,抽象成员
抽象类只能当作基类使用,用来在它的基础上定义子类
抽象类中有抽象方法,继承抽象类必须实现抽象方法。
1  | 
  | 
注意
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
this问题
类的方法经常用到this关键字,它表示该方法当前所在的对象。
有些场合需要给出this类型,但是 JavaScript 函数通常不带有this参数,这时 TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。
1  | // 编译前  | 
注意,this类型不允许应用于静态成员。
1  | class A {  | 
断言
对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。
1  | type T = 'a'|'b'|'c';  | 
类型断言有两种语法。
1  | // 语法一:<类型>值  | 
上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
1  | // 语法一  | 
类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
1  | const value:unknown = 'Hello World';  | 
另外,类型断言也适合指定联合类型的值的具体类型。
1  | const s1:number|string = 'hello';  | 
类型断言的条件
类型断言并不意味着,可以把某个值断言为任意类型。
1  | const n = 1;  | 
上面示例中,变量n是数值,无法把它断言成字符串,TypeScript 会报错。
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。
1  | expr as T  | 
上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:**expr是T的子类型,或者T是expr的子类型。**
如果真的要断言成一个完全无关的类型,也是可以做到的。
1  | // 或者写成 <T><unknown>expr  | 
实例
1  | const n = 1;  | 
as const 断言
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。
1  | // 类型推断为基本类型 string  | 
有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。
1  | let s = 'JavaScript';  | 
使用了as const断言以后,let 变量就不能再改变值了。
1  | let s = 'JavaScript' as const;  | 
注意,as const断言只能用于字面量,不能用于变量。
1  | let s = 'JavaScript';  | 
另外,as const也不能用于表达式。
1  | let s = ('Java' + 'Script') as const; // 报错  | 
as const断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。
1  | const v1 = {  | 
由于as const会将数组变成只读元组,所以很适合用于函数的 rest 参数。
1  | function add(x:number, y:number) {  | 
上面示例中,变量nums的类型推断为number[],导致使用扩展运算符...传入函数add()会报错,因为add()只能接受两个参数,而...nums并不能保证参数的个数。
解决方法就是使用as const断言,将数组变成元组。
1  | const nums = [1, 2] as const;  | 
使用as const断言后,变量nums的类型会被推断为readonly [1, 2],使用扩展运算符展开后,正好符合函数add()的参数类型。
非空断言
对于那些可能为空的变量(即可能等于undefined或null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!。
1  | function f(x?:number|null) {  | 
上面示例中,函数f()的参数x的类型是number|null,即可能为空。如果为空,就不存在x.toFixed()方法,这样写会报错。但是,开发者可以确认,经过validateNumber()的前置检验,变量x肯定不会为空,这时就可以使用非空断言,为函数体内部的变量x加上后缀!,x!.toFixed()编译就不会报错了。
class 中属性未赋值初始值时会报错,此时就可以使用 ! 去除报错
1  | class Point {  | 
另外,非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined或null。
断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
1  | function isString(value:unknown):void {  | 
上面示例中,函数isString()就是一个断言函数,用来保证参数value是一个字符串,否则就会抛出错误,中断程序的执行。
下面是它的用法。
1  | function toUpper(x: string|number) {  | 
为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。
1  | function isString(value:unknown):asserts value is string {  | 
上面示例中,函数isString()的返回值类型写成asserts value is string,其中**asserts和is都是关键词,value是函数的参数名,string是函数参数的预期类型**。它的意思是,该函数用来断言参数value的类型是string,如果达不到要求,程序就会在这里中断。
另外,断言函数的asserts语句等同于void类型,所以如果返回除了undefined和null以外的值,都会报错。
1  | function isString(value:unknown):asserts value is string {  | 
如果要将断言函数用于函数表达式,可以采用下面的写法。
1  | // 写法一  | 
模块
任何包含 import 或 export 语句的文件,就是一个模块(module)。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。
如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句。
1  | export {};  | 
上面这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码。
import type 语句
1  | // a.ts  | 
这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。
第一个方法是在 import 语句输入的类型前面加上type关键字。
1  | import { type A, a } from './a';  | 
上面示例中,import 语句输入的类型A前面有type关键字,表示这是一个类型。
第二个方法是使用 import type 语句,这个语句只用来输入类型,不用来输入正常接口。
1  | // 正确  | 
同样的,export 语句也有两种方法,表示输出的是类型。
1  | type A = 'a';  | 
mport type 语句也可以输入默认类型。
1  | import type DefaultType from 'moduleA';  | 
import type 在一个名称空间下,输入所有类型的写法如下。
1  | import type * as TypeNS from 'moduleA';  | 
importsNotUsedAsValues 编译设置
TypeScript 特有的输入类型(type)的 import 语句,编译成 JavaScript 时怎么处理呢?
TypeScript 提供了importsNotUsedAsValues编译设置项,有三个可能的值。
(1)remove:这是默认值,自动删除输入类型的 import 语句。
(2)preserve:保留输入类型的 import 语句。
(3)error:保留输入类型的 import 语句(与preserve相同),但是必须写成import type的形式,否则报错。
CommonJs模块
CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。
import = 语句
TypeScript 使用import =语句输入 CommonJS 模块。
1  | import fs = require('fs');  | 
上面示例中,使用import =语句和require()命令输入了一个 CommonJS 模块。模块本身的用法跟 Node.js 是一样的。
除了使用import =语句,TypeScript 还允许使用**import * as [接口名] from “模块文件”**输入 CommonJS 模块。
1  | import * as fs from 'fs';  | 
export = 语句
TypeScript 使用export =语句,输出 CommonJS 模块的对象,等同于 CommonJS 的module.exports对象。
1  | let obj = { foo: 123 };  | 
export =语句输出的对象,只能使用import =语句加载。
1  | import obj = require('./a');  | 
Classic 方法
Classic 方法以当前脚本的路径作为“基准路径”,计算相对模块的位置。
比如,脚本a.ts里面有一行代码import { b } from "./b",那么 TypeScript 就会在a.ts所在的目录,查找b.ts和b.d.ts。
至于非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。
比如,脚本a.ts里面有一行代码import { b } from "b",那么就会依次在每一级上层目录里面,查找b.ts和b.d.ts。
Node 方法
Node 方法就是模拟 Node.js 的模块加载方法,也就是require()的实现方法。
相对模块依然是以当前脚本的路径作为“基准路径”。比如,脚本文件a.ts里面有一行代码let x = require("./b");,TypeScript 按照以下顺序查找。
- 当前目录是否包含
b.ts、b.tsx、b.d.ts。如果不存在就执行下一步。 - 当前目录是否存在子目录
b,该子目录里面的package.json文件是否有types字段指定了模块入口文件。如果不存在就执行下一步。 - 当前目录的子目录
b是否包含index.ts、index.tsx、index.d.ts。如果不存在就报错。 
非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录node_modules。比如,脚本文件a.js有一行let x = require("b");,TypeScript 按照以下顺序进行查找。
- 当前目录的子目录
node_modules是否包含b.ts、b.tsx、b.d.ts。 - 当前目录的子目录
node_modules,是否存在文件package.json,该文件的types字段是否指定了入口文件,如果是的就加载该文件。 - 当前目录的子目录
node_modules里面,是否包含子目录@types,在该目录中查找文件b.d.ts。 - 当前目录的子目录
node_modules里面,是否包含子目录b,在该目录中查找index.ts、index.tsx、index.d.ts。 - 进入上一层目录,重复上面4步,直到找到为止。
 
路径映射
TypeScript 允许开发者在tsconfig.json文件里面,手动指定脚本模块的路径。
(1)baseUrl
baseUrl字段可以手动指定脚本模块的基准目录。
1  | {  | 
上面示例中,baseUrl是一个点,表示基准目录就是tsconfig.json所在的目录。
(2)paths
paths字段指定非相对路径的模块与实际脚本的映射。
1  | {  | 
上面示例中,加载模块jquery时,实际加载的脚本是node_modules/jquery/dist/jquery,它的位置要根据baseUrl字段计算得到。
注意,上例的jquery属性的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。
(3)rootDirs
rootDirs字段指定模块定位时必须查找的其他目录。
1  | {  | 
上面示例中,rootDirs指定了模块定位时,需要查找的不同的国际化目录。
装饰器
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@,后面是一个表达式。
(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象
装饰器有多种形式,基本上只要在@符号后面添加表达式都是可以的。下面都是合法的装饰器。
1  | 
  | 
注意,@后面的表达式,最终执行后得到的应该是一个函数
1  | function simpleDecorator(  | 
装饰器函数的两个参数
value:所装饰的对象。context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。
context 对象属性
(1)kind:字符串,表示所装饰对象的类型,可能取以下的值。
- ‘class’
 - ‘method’
 - ‘getter’
 - ‘setter’
 - ‘field’
 - ‘accessor’
 
这表示一共有六种类型的装饰器。
(2)name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
(3)addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。
(4)private:布尔值,表示所装饰的对象是否为类的私有成员。
(5)static:布尔值,表示所装饰的对象是否为类的静态成员。
(6)access:一个对象,包含了某个值的 get 和 set 方法。
类装饰器
类装饰器的类型描述如下。
1  | type ClassDecorator = (  | 
类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class。addInitializer方法为类添加初始化函数
类装饰器可以返回一个函数,替代当前类的构造方法。
1  | function countInstances(value:any, context:any) {  | 
新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。
类装饰器也可以返回一个新的类,替代原来所装饰的类。
1  | function countInstances(value:any, context:any) {  | 
上面示例中,@countInstances返回一个MyClass的子类。
下面的例子是通过类装饰器,禁止使用new命令新建类的实例。
1  | function functionCallable(  | 
方法装饰器
方法装饰器用来装饰类的方法(method)。它的类型描述如下。
1  | type ClassMethodDecorator = (  | 
根据上面的类型,方法装饰器是一个函数,接受两个参数:value和context。
参数value是方法本身,参数context是上下文对象,有以下属性。
kind:值固定为字符串method,表示当前为方法装饰器。name:所装饰的方法名,类型为字符串或 Symbol 值。static:布尔值,表示是否为静态方法。该属性为只读属性。private:布尔值,表示是否为私有方法。该属性为只读属性。access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。addInitializer():为方法增加初始化函数。
方法装饰器会改写类的原始方法,实质等同于下面的操作。跟切面编程很像,可以给方法加上前置后置方法
1  | function trace(decoratedMethod) {  | 
如果 trace 函数中返回一个新的函数,则替换 toString()
利用方法装饰器,可以将类的方法变成延迟执行。
1  | function delay(milliseconds: number = 0) {  | 
上面示例中,方法装饰器@delay(1000)将方法log()的执行推迟了1秒(1000毫秒)。
1  | class Person {  | 
上面例子中,类Person的构造方法内部,将this与greet()方法进行了绑定。如果没有这一行,将greet()赋值给变量g进行调用,就会报错了。
this的绑定必须放在构造方法里面,因为这必须在类的初始化阶段完成。现在,它可以移到方法装饰器的addInitializer()里面。
1  | function bound(  | 
属性装饰器
属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。只在类初始化时生效后续不会触发
1  | type ClassFieldDecorator = (  | 
属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。
1  | function logged(value, context) {  | 
getter 装饰器,setter 装饰器
getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。它们的类型描述如下。
1  | type ClassGetterDecorator = (  | 
注意,getter 装饰器的上下文对象context的access属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。
这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。
下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。
1  | class C {  | 
accessor 装饰器
装饰器语法引入了一个新的属性修饰符accessor。
1  | class C {  | 
accessor 装饰器的类型如下。
1  | type ClassAutoAccessorDecorator = (  | 
init()方法,用来改变私有属性的初始值。
上面的代码等同于下面的代码。
1  | class C {  | 
accessor也可以与静态属性和私有属性一起使用。
装饰器执行顺序
装饰器的执行分为两个阶段。
(1)评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。
(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
1  | function d(str:string) {  | 
上面示例中,类T有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。
它的运行结果如下。
1  | 评估 @d(): 类装饰器  | 
declare
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
declare 关键字可以描述以下类型。
- 变量(const、let、var 命令声明)
 - type 或者 interface 命令声明的类型
 - class
 - enum
 - 函数(function)
 - 模块(module)
 - 命名空间(namespace)
 
declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。
declare variable
declare 关键字可以给出外部变量的类型描述。
举例来说,当前脚本使用了其他脚本定义的全局变量x。
1  | x = 123; // 报错  | 
上面示例中,变量x是其他脚本定义的,当前脚本不知道它的类型,编译器就会报错。
这时使用 declare 命令给出它的类型,就不会报错了。
1  | declare let x:number;  | 
如果没有指定类型就默认为any
注意,declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值,即不能涉及值。
1  | // 报错  | 
declare function
declare 关键字可以给出外部函数的类型描述。
1  | declare function sayHello(  | 
上面示例中,declare 命令给出了sayHello()的类型描述,因此可以直接使用它。
注意,这种单独的函数类型声明语句,只能用于declare命令后面。一方面,TypeScript 不支持单独的函数类型声明语句;另一方面,declare 关键字后面也不能带有函数的具体实现。
1  | // 报错  | 
declare class
declare 给出 class 类型描述的写法如下。
1  | declare class Animal {  | 
同样的,declare 后面不能给出 Class 的具体实现或初始值。
declare module,declare namespace
如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。
1  | declare namespace AnimalLib {  | 
declare module 和 declare namespace 里面,加不加 export 关键字都可以。
declare 关键字的另一个用途,是为外部模块添加属性和方法时,给出新增部分的类型描述。
1  | import { Foo as Bar } from 'moduleA';  | 
declare global
如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用declare global {}语法。
1  | export {};  | 
declare enum
declare 关键字给出 enum 类型描述的例子如下,下面的写法都是允许的。
1  | declare enum E1 {  | 
命名空间
避免全局污染
方便拓展:多个同名的 namespace 会自动合并,这一点跟 interface 一样。
注意:合并时同名成员会导致报错
1  | export namespace Home {  | 
d.ts类型声明文件
 单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。
举例来说,有一个模块的代码如下。
1  | const maxInterval = 12;  | 
它的类型声明文件可以写成下面这样。
1  | export function getArrayLength(arr: any[]): number;  | 
下面是一个如何使用类型声明文件的简单例子。有一个类型声明文件types.d.ts。
1  | // types.d.ts  | 
然后,就可以在 TypeScript 脚本里面导入该文件声明的类型。
1  | // index.ts  | 
类型声明文件也可以包括在项目的 tsconfig.json 文件里面,这样的话,编译器打包项目时,会自动将类型声明文件加入编译,而不必在每个脚本里面加载类型声明文件。比如,moment 模块的类型声明文件是moment.d.ts,使用 moment 模块的项目可以将其加入项目的 tsconfig.json 文件。
1  | {  | 
类型声明文件的来源
类型声明文件主要有以下三种来源。
- TypeScript 编译器自动生成。
 - TypeScript 内置类型文件。
 - 外部模块的类型声明文件,需要自己安装。
 
自动生成
只要使用编译选项declaration,编译器就会在编译时自动生成单独的类型声明文件。
下面是在tsconfig.json文件里面,打开这个选项。
1  | {  | 
你也可以在命令行打开这个选项。
1  | tsc --declaration  | 
内置声明文件
安装 TypeScript 语言时,会同时安装一些内置的类型声明文件,主要是内置的全局对象(JavaScript 语言接口和运行环境 API)的类型声明。
这些内置声明文件位于 TypeScript 语言安装目录的lib文件夹内,数量大概有几十个,下面是其中一些主要文件。
- lib.d.ts
 - lib.dom.d.ts
 - lib.es2015.d.ts
 - lib.es2016.d.ts
 - lib.es2017.d.ts
 - lib.es2018.d.ts
 - lib.es2019.d.ts
 - lib.es2020.d.ts
 - lib.es5.d.ts
 - lib.es6.d.ts
 
这些内置声明文件的文件名统一为“lib.[description].d.ts”的形式,其中description部分描述了文件内容。比如,lib.dom.d.ts这个文件就描述了 DOM 结构的类型。
如果开发者想了解全局对象的类型接口(比如 ES6 全局对象的类型),那么就可以去查看这些内置声明文件。
TypeScript 编译器会自动根据编译目标target的值,加载对应的内置声明文件,所以不需要特别的配置。但是,可以使用编译选项lib,指定加载哪些内置声明文件。
1  | {  | 
上面示例中,lib选项指定加载dom和es2021这两个内置类型声明文件。
编译选项noLib会禁止加载任何内置声明文件。
外部类型声明文件
如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。
这时又分成三种情况。
(1)这个库自带了类型声明文件。
比如moment这个库就自带moment.d.ts。使用这个库可能需要单独加载它的类型声明文件。
(2)这个库没有自带,但是可以找到社区制作的类型声明文件。
第三方库如果没有提供类型声明文件,社区往往会提供。TypeScript 社区主要使用 DefinitelyTyped 仓库,各种类型声明文件都会提交到那里,已经包含了几千个第三方库。
(3)找不到类型声明文件,需要自己写。
比如,使用 jQuery 的脚本可以写成下面这样。
1  | declare var $:any  | 
上面代码表示,jQuery 的$对象是外部引入的,类型是any,也就是 TypeScript 不用对它进行类型检查。
也可以采用下面的写法,将整个外部模块的类型设为any。
1  | declare module '模块名';  | 
有了上面的命令,指定模块的所有接口都将视为any类型。
三斜杠命令
如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。
举例来说,入口文件是main.d.ts,里面的接口定义在interfaces.d.ts,函数定义在functions.d.ts。那么,main.d.ts里面可以用三斜杠命令,加载后面两个文件。
1  | /// <reference path="./interfaces.d.ts" />  | 
三斜杠命令主要包含三个参数,代表三种不同的命令。
path
/// <reference path="" />是最常见的三斜杠命令,告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。types
types 参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在
node_modules/@types目录。lib
/// <reference lib="..." />命令允许脚本文件显式包含内置 lib 库,等同于在tsconfig.json文件里面使用lib属性指定 lib 库。
ts类型运算符
keyof 运算符
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
1  | type MyObj = {  | 
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。
1  | // string | number | symbol  | 
对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。
对于联合类型,keyof 返回成员共有的键名。
1  | type A = { a: string; z: boolean };  | 
keyof 运算符往往用于精确表达对象的属性类型。
举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。
1  | function prop(obj, key) {  | 
上面这个函数添加类型,只能写成下面这样。
1  | function prop(  | 
上面的类型声明有两个问题,一是无法表示参数key与参数obj之间的关系,二是返回值类型只能写成any。
有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。
1  | function prop<Obj, K extends keyof Obj>(  | 
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
1  | type NewProps<Obj> = {  | 
上面示例中,类型NewProps是类型Obj的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了boolean。
下面的例子是去掉 readonly 修饰符。
1  | type Mutable<Obj> = {  | 
对应地,还有+readonly的写法,表示添加只读属性设置。
in 运算符
1  | const obj = { a: 123 };  | 
上面示例中,in运算符用来判断对象obj是否包含属性a。
in运算符的左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。
TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
1  | type U = 'a'|'b'|'c';  | 
上面示例中,[Prop in U]表示依次取出联合类型U的每一个成员。
方括号运算符
方括号运算符([])用于取出对象的 键值类型,比如T[K]会返回对象T的属性K的类型。
1  | type Person = {  | 
上面示例中,Person['age']返回属性age的类型,本例是number。
方括号的参数如果是联合类型,那么返回的也是联合类型。
1  | type Person = {  | 
如果访问不存在的属性,会报错。
方括号运算符的参数也可以是属性名的索引类型。
1  | type Obj = {  | 
上面示例中,Obj的属性名是字符串的索引类型,所以可以写成**Obj[string],代表所有字符串属性名**,返回的就是它们的类型number。
extends…?: 条件运算符
条件运算符extends...?:可以根据当前类型是否符合某种条件,返回不同的类型。
1  | T extends U ? X : Y  | 
上面式子中判断T是否为U的子类型,这里的T和U可以是任意类型。成立 结果类型为X,否则结果类型为 Y
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。返回新的联合类型
1  | (A|B) extends U ? X : Y  | 
如果不希望联合类型被条件运算符展开,可以把extends两侧的操作数都放在方括号里面。
1  | // 示例一  | 
infer 关键字
infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。
1  | type Flatten<Type> =  | 
上面示例中,
infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入
而Flatten<Type>则表示Type这个类型参数是外部传入的。
Type extends Array<infer Item>则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item,即Item是从Type推断出来的。
1  | // string  | 
is 运算符
is运算符用来描述返回值属于true还是false。
1  | function isFish(  | 
上面示例中,函数isFish()的返回值类型为pet is Fish,表示如果参数pet类型为Fish,则返回true,否则返回false。
is运算符可以用于类型保护。
1  | function isCat(a:any): a is Cat {  | 
is运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值。
1  | class Teacher {  | 
上面示例中,isStudent()方法的返回值类型,取决于该方法内部的this是否为Student对象。如果是的,就返回布尔值true,否则返回false。
注意,this is T这种写法,只能用来描述方法的返回值类型,而不能用来描述属性的类型。
模板字符串
模板字符串的最大特点,就是内部可以引用其他类型。
1  | type World = "world";  | 
上面示例中,类型Greeting是一个模板字符串,里面引用了另一个字符串类型world,因此Greeting实际上是字符串hello world。
注意,模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined。引用这6种以外的类型会报错。
satisfies 运算符
举例来说,有一个对象的属性名拼写错误。
1  | const palette = {  | 
上面示例中,对象palette的属性名拼写错了,将blue拼成了bleu,我们希望通过指定类型,发现这个错误。
1  | type Colors = "red" | "green" | "blue";  | 
上面示例中,变量palette的类型被指定为Record<Colors, string|RGB>,这是一个类型工具,用来返回一个对象
这样的写法,虽然可以发现属性名的拼写错误,但是带来了新的问题。
1  | const greenComponent = palette.green.substring(1, 6); // 报错  | 
这时就可以使用satisfies运算符,对palette进行类型检测,但是不改变 TypeScript 对palette的类型推断。
1  | type Colors = "red" | "green" | "blue";  | 
上面示例中,变量palette的值后面增加了satisfies Record<Colors, string|RGB>,表示该值必须满足Record<Colors, string|RGB>这个条件,所以能够检测出属性名bleu的拼写错误。同时,它不会改变palette的类型推断,所以,TypeScript 知道palette.green是一个字符串,对其调用substring()方法就不会报错。
satisfies也可以检测属性值。
1  | const palette = {  | 
上面示例中,属性blue的值只有两个成员,不符合元组RGB必须有三个成员的条件,从而报错了。
类型映射
映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。
举例来说,现有一个类型A和另一个类型B。
1  | type A = {  | 
上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,逐个写起来就很麻烦。
使用类型映射,就可以从类型A得到类型B。
1  | type A = {  | 
上面示例中,类型B采用了属性名索引的写法,[prop in keyof A]表示依次得到类型A的所有属性名,然后将每个属性的类型改成string。
在语法上,[prop in keyof A]是一个属性名表达式,表示这里的属性名需要计算得到。具体的计算规则如下:
prop:属性名变量,名字可以随便起。in:运算符,用来取出右侧的联合类型的每一个成员。keyof A:返回类型A的每一个属性名,组成一个联合类型。
新版本中还能修改映射的键名,过滤属性及联合类型映射等
类型工具
TypeScript 提供了一些内置的类型工具,用来方便地处理各种类型,以及生成新的类型。
TypeScript 内置了17个类型工具,可以直接使用。
AwaitedConstructorParametersExcludeExtractInstanceTypeNonNullableOmitOmitThisParameterParametersPartialPickReadonlyRecordRequiredReadonlyArrayReturnTypeThisParameterTypeThisType- 字符串类型工具
 
ts注释指令
TypeScript 接受一些注释指令。
所谓“注释指令”,指的是采用 JS 双斜杠注释的形式,向编译器发出的命令。
ts.config.js
tsconfig.json是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有tsconfig.json,TypeScript 就认为这是项目的根目录。如果项目源码是 JavaScript,但是想用 TypeScript 处理,那么配置文件的名字是
jsconfig.json,它跟tsconfig的写法是一样的。
编译
1  | npm install -g typescript  | 
- typescript 模块 可以编译 ts 为 js代码。
 
1  | tsc file1.ts file2.ts  | 
ts.confg.js
1  | {  | 
有了这个配置文件,编译时直接调用tsc命令就可以了。
1  | tsc  | 
- ts-node 模块
 
可以,直接编译
类型推断问题
ts 会根据值自动类型推断,当无法推断出来类型时则会认为是 any 类型。
在线测试
tsc 命令
tsc 是 TypeScript 官方的命令行编译器,用来检查代码,并将其编译成 JavaScript 代码。
tsc 默认使用当前目录下的配置文件tsconfig.json,但也可以接受独立的命令行参数。命令行参数会覆盖tsconfig.json,比如命令行指定了所要编译的文件,那么 tsc 就会忽略tsconfig.json的files属性。
- 本文作者: 王不留行
 - 本文链接: https://wyf195075595.github.io/2022/07/17/programming/javascript/typescript/
 - 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!
 
		
                
                LiYongci
              
                
                衔蝉
              
                
                哈希米