typescript基础
TypeScript 入门
TSC 编译器
1 | npm i -g typescript |
1 | // hello.ts |
1 | tsc hello.ts |
发出错误
--noEmitOnError不允许发生错误,一旦发生错误将不会生成js文件
1 | tsc --noEmitOnError hello.ts |
显式类型
- 明确指定参数或变量的类型
- 但是我们不总是需要编写明确的类型,在大多数情况下,
TypeScript会自动推断代码中的类型
1 | function greet(person: string, data: Date) { |
擦拭类型
tsc编译后的js将类型擦除掉- 类型注释永远不会改变程序的运行时行为
降级编译
tsc指定编译生成js的版本tsc --target es2015 hello.ts
严格模式
tsc --strict true hello.ts开启严格模式tsc --strict true --noImplictAny true开启严格模式,并且当类型隐式推断为any时发出错误tsc --target es2015 --strictNullChecks truenull和undefined可以分配给任意类型,这可能会导致空异常,开启strictNullChecks可以防止出现空异常
常用类型
typescript
1 | tsc --init |
1 | { |
基元类型 number string boolean
1 | let a: number = 123; |
数组
1 | let arr: number[] = [1, 2, 3]; |
any
- 当不希望某个特定值导致类型检查错误时,可以使用
any - 当一个值的类型是
any时,可以访问它的任何属性,将它分配给任何类型的值 - 但在运行环境下执行代码可能是错误的
- 当我们不指定类型时,并且
typescript无法从上下文推断它时,编译器通常会默认为any,但是通常情况下,我们需要避免这种情况,因为any没有进行类型检查,使用noImplicitAny将任何隐式标记any为错误
1 | let obj: any = { |
变量上的类型注释
- 可以选择添加类型注释来显式指定变量的类型
- 但是这不是必须的,因为
typescript会尝试自动推断代码中的类型
1 | let myName: string = "suxi"; |
函数
参数类型注释
- 即便没有参数类型注释,仍然会检查参数的数量
1 | function greet(name: string) { |
返回类型注释
- 通常不需要返回类型,因为
typescript会推断出返回类型
1 | function getNumber(): number { |
匿名函数
- 匿名函数与函数声明有所不同,当一个函数出现在
typescript可以确定它将如何被调用的地方时,该函数的参数会自动指定类型 - 上下文推断类型,函数发生在其中的上下文通知它应该具有什么类型
1 | const names: Array<string> = ["abc", "def"]; |
对象类型
- 下面是一个对象类型的例子
x和y是对象的属性,它们的类型为number- 可以使用
,或;分隔属性,最后一个分割符是可选的 - 每个属性的类型部分也是可选的,如果不指定类型,则将假定为
any
1 | let point: { x: number; y: number; } = { |
可选属性
- 对象类型还可以指定其部分或全部属性是可选的
- 在属性后添加
?表示其可选属性 - 当使用可选属性时,首先要判断是否存在
1 | let point: { x: number; y: number; z?: number } = { |
联合类型
typescript 允许使用多种运算符从现有类型中构建新类型
定义联合类型
- 联合类型是由多个其他类型组成的类型,表示可以是这些类型中的任何一种类型
- 这些类型中的每一种类型称为联合类型的成员
1
2
3
4
5function print(id: number | string) {
console.log(id);
}
print(123);
print("123");使用联合类型
- 提供联合类型很容易,但是使用时,如果联合的每个成员都有效,
ts将只允许使用联合做一些事情,例如,如果string | number联合类型,不能只使用一种类型的操作 - 而是使用代码缩小联合,就像没有类型注释的
js一样,当ts可以根据代码结构为值推断为具体的类型时,就会发生缩小 - 当然有时对于一个
union所有成员都有一些共同点,例如,数组和字符串都有一个slice方法那么使用该属性就可以不会缩小范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// error
function print(id: number | string) {
console.log(id.toUpperCase())
}
function print(id: number | string) {
if (typeof id === "string") {
// id 为 string
console.log(id.toUpperCase())
} else {
// id 为 number
console.log(id);
}
}
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}- 提供联合类型很容易,但是使用时,如果联合的每个成员都有效,
类型别名
- 如果我们想要多次使用一个类型,恰好这个类型的定义又很复杂,我们可以使用类型别名来声明类型,以便于多次引用它
- 当然类型别名只是别名,不能使用类型别名来创建相同类型的不同版本
1 | type Point = { |
接口
- 接口声明是另一种方式来命名对象类型
ts只关注类型的结构和功能
1 | interface Point { |
接口和类型别名之间的差异
- 类型别名和接口非常相似,在多数情况下可以自由的选择它们,几乎所有功能都在
interface中可用type关键区别在于扩展新类型的方式不同 - 类型别名可能不参与声明合并,但接口可以
- 接口只能用于声明对象的形状,不能重命名基元
- 接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时
1 | // 扩展接口 |
1 | // 通过交叉点扩展类型 |
1 | // 向现有类型添加新字段 |
1 | // 类型创建后不可更改 |
类型断言
有时,我们会获得有关
TS不知道的值类型的信息,例如document.getElementById,TS只知道它将返回某种类型的HTMLElement但我们自己知道它将始终返回HTMLCanvasElement类型与穷尽性检查,这种情况下,我们需要类型断言来指定更加具体的类型1
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
和类型注释一样,类型断言不会影响代码的运行行为并由编译器删除断言,当然也可以使用
<>进行断言,它们是等效的,但是需要注意这种方式不可以用在tsx中1
const canvas = <HTMLCanvasElement>document.getElementById("canvas");
TS只允许断言转换为更具体或不太具体的类型版本,此规则可防止不可能的断言1
2
3
4// 不可能的断言 因为这两种类型没有充分重叠
const x = "hello" as number;
// 需要先断言为 unknown 或 any 类型 在断言为 number 类型
const (x = "hello" as unknown) as number;
文字类型
除了一般类型
stringnumber,我们还可以在类型位置引用特定的字符串和数字一种方法是考虑
js如何以不同的方式声明变量varlet两者都允许更改变量中保存的内容const不允许,这反映在ts如何为文字创建类型上
1
2
3
4
5
6
7
8
9
10let testString = "hello world";
testString = "123"; // 可以任意更改 相当于 testString的类型是 string
const constantString = "hello world";
// 已经不能在更改了,它只能表示一个可能的字符串,所有实际上 constantString 的类型是 "hello world" 这就是文字类型
// 但就其本身而言,文字类型并不是很有价值
let x: "hello" = "hello";
x = "hello"; // 正确
x = "xxxx"; // 错误拥有一个只能有一个值的变量并没有多大用处,但是通过将文字组合成联合类型,可以用来表达一个更有用的概念 — 例如只接受一组特定的值
当然文字类型也可以和非文字类型结合使用
还有一种文字类型:布尔文字,只有两种布尔文字类型,它们是类型
true和false注意,此时他们是文字类型,不是值。那么基元boolean也本身是联合类型true|false的别名,可以这样理解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function printText(s: string, align: "left" | "right") {
console.log(s, align);
}
printText("hello", "left");
printText("hello", "right");
printText("hello", "center"); // 错误
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
interface Options {
width: number;
}
function configure(x: Options | "auto") {}
configure({ width: 100 });
configure("auto");
let auto = "auto"; // 注意 这是 string 隐式 它不是文字类型 而是基元 string 不能直接 configure(auto) 这是错误的
// configure(auto);
文字推理
当我们使用对象初始化变量时,
TS假定该对象的属性稍后可能会更改值- 所以
const定义对象时,并不认为counter就是一个文字类型,而是认为它是一个基元number,所以我们改变counter的值,ts并不认为它是错误的 - 这也符合
js中的行为
1
2
3
4const obj = { counter: 0 }; // 注意对象 const counter 属性可能改变
obj.counter = 1;
obj.counter = "number"; // 错误- 所以
上面的行为同样适用于字符串
可以使用
as const将整个对象转换为类型文字,就是说,确保了对象的所有属性分配的都是文字类型,而不是一个更一般的stringnumber等1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function handleRequest(url: string, method: "GET" | "POST") {
}
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // error req.method 不是 "GET" | "POST" 类型 而是 string 类型
// 两种方式解决
// 1. 在任意位置使用类型断言
// 1.1 第一种 始终保持 req.method 拥有文字类型 "GET" 防止之后可能赋值其他类型 例如 POSt
const req = { url: "https://example.com", method: "GET" as "GET" };
// 1.2 第二种 我知道有其他原因 `req.method` 具有 GET 值
handleRequest(req.url, req.method as "GET");
// 2. 将整对象转为文字类型
const req = { url: "https://example.com", method: "GET" } as const;
// req的类型实际上是 { url: "https://example.com"; method: "GET" } 可以这样理解
null 和 undefined
js中null和undefined表示不存在或未初始化的值ts中有两个对应的同名类型,这些类型的行为取决于是否设置了strictNullChecks选项- 关闭
false依然可以正常访问的值,并且可以将值分配给任何类型的属性。这类似没有空检查的语言 (c#java),缺少空检查往往是错误的主要来源 - 开启,需要在对值使用之前测试这些值,就像在使用可选属性之前检查一样,我们需要使用缩小来检查可能的值
1
2
3
4
5
6
7
8
9function doSome(x: string | null) {
if (x === null) {
console.log("x is null");
} else {
console.log("x is string");
console.log(x.length);
console.log(x.toUpperCase());
}
}- 关闭
非空断言运算符 (
!后缀)ts的特殊语法,可以在不进行任何显式检查下,使用值!在任何表达式之后写入实际上是一种类型断言,表示该值 我知道它由于某种原因它不是null或undefined- 就像类型断言一样,它不会更改代码的运行时行为,因此仅当你知道它不是
null或undefined时才应该使用非空断言运算符,这才是重要的
1
2
3function liveDangerous(x?: number | null) {
console.log(x!.toFixed(2)); // 我知道 x 是 number 类型
}
枚举
- 枚举是
ts添加到js中的一个功能,它允许描述一个值,该值是一组可能的命名常量之一。 - 关于枚举更深层次的使用
1 | enum Direction { |
1 | var Direction; |
不太常见的原语
bigint
- 从
es2020开始,js中有一个用来表示非常大的整数的原语BigInt
1 | let bigNumber1: bigint = BigInt(100); |
symbol
Symbol用来通过函数创建全局唯一引用
1 | let sy1: symbol = Symbol("foo"); |
类型缩小
1 | function padLeft(padding: number | string, input: string) { |
typeof 类型守卫
typeof和js运算符一样,它可以提供有关我们运行时拥有的值类型的非常基本的信息。ts期望它返回一组特定的字符串stringnumberbigintbooleansymbolundefinedobjectfunction
- 在
TS使用typeof可以理解为,它缩小在不同分支中的类型 - 在
typescript中检查typeof的返回值是一种类型保护 - 注意
typeof不返回字符串null
1 | function printAll(strs: string | string[] | null) { |
真值缩小
真值检查通常在
js我们也会这样做:&&||if!等表达式1
2
3
4
5
6
7function getPersonCount(count: number) {
if (count) {
console.log(`There are ${count} people`);
} else {
console.log("There are no people");
}
}这样通过
if语句将它们的条件强制转化为boolean使它有意义,然后根据结果是truefalse来选择它们的分支下面这些值将会强制转换为
false。其他值被转换为true,你始终可以在Boolean函数中运行值获得boolean或使用较短的双布尔否定将值强制转换为boolean,当然双重否定的优点在于ts将它推断为一个true的文字类型 比较狭窄 而Boolean是一个boolean类型0NaN""0nnullundefined
1
2Boolean("hello"); // boolean
!!"1"; // true 类型利用真值缩小可以防范于
nullundefined之类的值的影响1
2
3
4
5
6
7
8
9
10
11function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do something
}
}但是对原语的真值检查通常容易出错
1
2
3
4
5
6
7
8
9
10
11
12function printAll(strs: string | string[] | null) {
// 这样排除了 空字符串和 null
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}1
2
3
4
5
6
7
8// 一个真值缩小的例子
function multiplyAll(values: number[] | undefined, factor: number) {
if (!values) {
return [];
} else {
return values.map((x) => x * factor);
}
}
等值缩小
使用
===!==!===等值检查来实现类型缩小,叫等值缩小1
2
3
4
5
6
7
8
9
10function example(x: string | number, y: string | boolean) {
if (x === y) {
// 我们可以确定 x 和 y 具有相同的类型 string
x.toUpperCase(); // 因为 x 和 y 具有相同的类型 所以可以调用 toUpperCase()
y.toUpperCase(); // 因为 x 和 y 具有相同的类型 所以可以调用 toUpperCase()
} else {
console.log(x);
console.log(y);
}
}在真值缩小中我们使用了一个不完善的缩小从而将空字符串从其中排除掉了,那么我们可以使用等值缩小进行完善
1
2
3
4
5
6
7
8
9
10
11function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}js更宽松的相等性检查==!=也能正确缩小。如果要检查一个变量是否等于null或undefined那么使用!=或==是一个好的方法1
2
3
4
5
6
7
8
9
10interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
if (container != null) {
console.log(container.value);
container.value *= factor;
}
}
in 操作符缩小
in运算符,用于确定对象是否拥有某个名称的属性value in xvalue是字符串文字,x是类型,值为true的分支缩小,需要x具有可选或必需属性的类型的值,值为false的分支缩小,需要具有可选或缺失属性的值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
type Human = { swim?: () => void; fly?: () => void };
function move1(animal: Human | Fish | Bird) {
if ("swim" in animal) {
// Fish Human
}
if ("fly" in animal) {
// Bird Human
}
}
instanceof 操作符缩小
instanceof检查一个值是否是另一个值的实例。更具体的x instanceof Foo检查x的原型链中是否含有Foo.prototype
1 | function logValue(x: Date | string) { |
分配缩小
我们在为任何变量赋值时,
TypeScript会查看赋值的右侧并适当缩小左侧1
2
3
4
5
6
7
8
9let x = Math.random() < 0.5 ? 10 : "hello world"; // string | number
x = 1;
console.log(x);
x = "goodbye";
console.log(x);
x = false // error注意,这些分配中的每一个都是有效的,即使在我们第一次赋值后观察到
x更改为number我们仍然可以将string赋值给x,这是因为x在声明时是string | number
控制流分析
- 通过分析代码流程进行缩小类型
1 | function padLeft(padding: number | string, input: string) { |
使用类型谓词
为了定义一个用户定义的类型保护,我们需要定义一个函数,其返回类型是一个类型谓词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55type Fish = {
name: string;
swim: () => void;
}
type Bird = {
name: string;
fly: () => void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// pet is Fish 是类型谓词 形式是 `parameterName is Type` 其中 `parameterName` 是一个形式参数的名称,`Type` 是一个类型名称
// 任何时候 isFish 被调用时,如果原始类型是兼容的,`ts` 将把变量缩小到该特定类型
function getSmallPet(): Fish | Bird {
let fish: Fish = {
name: "gold fish",
swim: () => {
}
}
let bird: Bird = {
name: "bird",
fly: () => {
}
}
return true ? bird : fish;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// 等同于
// const underWater1: Fish[] = zoo.filter(isFish) as Fish[];
const underWater2: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "frog") {
return false;
}
return isFish(pet);
})
受歧视的 unions
1 | interface Shape { |
那么我们需要将 Shape 分成两种类型
1 | type Circle = { |
- 当联合类型中的每个类型都包含一个与文字类型相同的属性时,
ts认为这是一个有区别的联合类型,并且可以缩小联合类型的成员 - 上面的例子
kind是公共成员,检查kind是circle就可以剔除Shape中所没有circle类型属性的类型,同样的检查也适用于switch语句
1 | function getArea(shape: Shape) { |
never 类型与穷尽性检查
在缩小范围中,可以将一个联合体的选项减少到已经删掉了所有可能性并且什么都不剩的程度,在这种情况下,我们可以将其称为
never类型 不存在的状态1
2
3
4
5
6
7
8
9
10
11function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
函数更多
函数是任何程序的基本构件
函数类型表达式
- 描述一个函数的最简单的方法就是用一个函数类型表达式。这些类型在语法上类似于箭头函数
1 | function greeter(fn: (a: string) => void) { |
调用签名
- 在
js中,函数除了可以调用以外,还可以拥有属性。然后,函数类型表达式的语法不允许声明属性。如果我们想用属性来描述可调用的东西,我们可以在一个对象类型中写入一个调用签名 - 注意,和函数类型表达式相比,语法略有不同:在参数列表和返回类型之间使用
:而不是=>
1 | type DescriptionFunction = { |
构造签名
js函数也可以使用new操作符来调用,typescript称为构造函数,它们通常会创建一个新的对象,你可以通过在调用签名前面添加new关键字来写一个构造签名1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Ctor {
s: string;
constructor(s: string) {
this.s = s;
}
}
type SomeConstructor = {
new (s: string): Ctor;
}
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
const f = fn(Ctor);
console.log(f.s);还有些对象,如
Date对象,可以在有new或没有new的情况下被调用,那么可以在同一类型中结合调用签名和构造签名1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49interface DateFunction {
new (s: string): Date;
(t: number): Date;
}
function createDate(fn: DateFunction): Date {
let d = new fn("2024-12-10");
let t = fn(100);
return d;
}
interface ClockConstructor {
new (hour: number, minute: number): ClockInerface;
}
interface ClockInerface {
tick(): void;
}
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInerface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInerface {
constructor(h: number, m: number) {
}
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInerface {
constructor(h: number, m: number) {
}
tick() {
console.log("tick tock");
}
}
let c1 = createClock(DigitalClock, 12, 17);
let c2 = createClock(AnalogClock, 7, 32);
c1.tick();
c2.tick();
泛型函数
在写一个函数时,输入的类型与输出的类型有关,或者两个输入的类型以某中方式相关,那么就可以使用泛型函数
1
2
3
4
5
6
7
8
9
10
11function firstElement(arr: any[]): any {
return arr[0];
}
function firstElement1<T>(arr: T[]): T {
return arr[0];
}
console.log(firstElemet([]));
console.log(firstElemet([1, 2, 3]));
console.log(firstElemet(["str1"]));
类型推断
- 在上面的例子中我们没有指定类型,类型是由
ts自动推断出来的,当然也可以使用多个类型参数
1 | function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] { |
限制条件
我们可以对任何类型的值进行操作,有时我们想把两个值联系起来,但只能对某个值的子集进行操作,在这种情况下,我们可以使用一个约束条件来限制一个类型参数可以接受的类型
1
2
3
4
5
6
7
8
9
10
11function longest<T extends { length: number }>(a: T, b: T) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
console.log(longest([1, 2], [1, 2, 3]));
console.log(longest("hello", "world123"));
longest(10, 20); // error在上面的例子中,有一些有趣的事情需要注意,我们允许
Ts推断longest的返回类型,返回类型推断也适用于通用函数,我们将T约束为{ length: number },所以我们才可以访问ab两个参数的.length属性,如果没有类型约束,我们不可以访问这些属性,因为这些值可能是一些没有长度属性的其他类型Arraystring的类型是根据参数推断出来的,所以泛型就是把两个或多个具有相同类型的值联系起来最后,正如我们希望的,
longest(10, 20)会报错,因为数字类型没有.length属性
使用受限值
这里有一个适用通用约束条件的常见错误
1
2
3
4
5
6
7function minimumLength<T extends { length: number }>(obj: T, minimum: number): T {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
}
}看起来上面的函数是没有问题的,
T被限制为{ length: number },而且这个函数要么返回T要么返回一个与该限制相匹配的值,问题是,该函数承诺返回与传入参数相同的类型,而不仅仅是和约束条件想匹配的对象,如果这段代码是合法的,那么你可以得到这样一个无法工作的代码1
2const arr = minimumLength([1, 2, 3], 4);
console.log(arr.slice(1));
指定类型参数
TS通常可以推断出通用调用中的预期类型参数,但并非总是如此1
2
3
4
5
6
7
8function combine<T>(arr1: T[], arr2: T[]) {
return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]); // error 因为T首先被推断为 number
// 如果你非要这样做 指定类型参数是有必要的
const arr1 = combine<string | number>([1, 2, 3], ["hello"]);
编写优秀通用函数的准则
编写泛型函数很有趣,但是很容易被类型参数所迷惑,有太多的类型参数或在不需要的地方使用约束会使推理不那么成功
类型参数下推
- 规则: 在可能的情况下,使用类型参数本身,而不是对其进行约束
1
2
3
4
5
6
7
8
9
10
11
12
13function firstElement1<T>(arr: T[]) {
return arr[0];
}
function firstElement2<T extends any[]>(arr: T) {
return arr[0];
}
// 第一个函数推断返回类型是 T 而第二个函数推断返回 any
// 因为 ts 必须使用约束类型来解析 arr[0] 表达式,而不是在调用期间 等待解析该元素
let a = firstElement1([1, 2, 3]); // number
let b = firstElement2([1, 2, 3]); // any使用更少的类型参数
- 规则: 总是尽可能少的使用类型参数
1
2
3
4
5
6
7
8function filter1<T>(arr: T[], func: (arg: T) => boolean): T[] {
return arr.filter(func);
}
// 这种形式除了让函数更难看懂,没有任何用处
function filter2<T, Func extends (arg: T) => boolean>(arr: T[], fn: Func): T[] {
return arr.filter(fn);
}类型参数应出现两次
- 规则: 如果一个类型的参数只出现在一个地方,请重新考虑是否真的需要
1
2
3
4
5
6
7
8
9
10function greet<Str extends string>(s: Str) {
console.log("hello, " + s);
}
greet("world");
// 为什么不直接使用 string 类型呢
function greet(s: string) {
console.log("hello, " + s);
}
可选参数
js中的函数经常需要一个可变数量的参数1
2
3
4
5
6
7
8
9
10
11function f(n: number) {
console.log(n.toFixed()); // 0 个参数
console.log(n.toFixed(1)); // 1 个参数
}
function fn1(x?: number) {
// ...
}
fn1(); // 正确
fn1(10); // 正确上面
fn1函数参数x虽然指定为number但因为它是可选参数,所以它实际上具有number | undefined类型也可以提供默认参数值,现在
fn1的主体中,x将具有number类型,因为任何undefined类型都会被替换为10请注意,当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个丢失的参数
1
2
3
4
5
6
7
8
9function fn1(x = 10) {
// ...
}
declare function f(x?: number): void;
f();
f(10);
f(undefined);
回调中的可选参数
一旦了解可选参数和函数类型表达式,在编写调用回调的函数时就很容易犯错
规则: 当为回调写一个函数类型时,永远不要写一个可选参数,除非你打算在不传递该参数的情况下它还可以工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
// 我们在书写 index? 作为一个可选参数时,通常是想让这些调用是合法的
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));
// 可是,如果调用者不想提供索引,并且回调中使用索引, 那么就会出现错误
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i]);
}
}
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed()); // error 因为 i 可能是 undefined 所以要让它不报错,要么你确定它一定有值使用非空断言 或者使用类型缩小
})
函数重载
当一些函数可以在不同的参数数量和类型中被调用,可以通过编写重载签名来指定一个可以不同方式调用的函数,要做到这一点,要写一些数量的函数签名(通常是两个或多个),然后是函数的主体
1
2
3
4
5
6
7
8
9
10
11
12
13function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
makeDate(12345678);
makeDate(3, 2, 2024);
makeDate(1, 3); // error这里我们写了两个重载: 一个接受一个参数,另一个接受三个参数。前两个签名被称为重载签名,然后我们写了一个具有兼容签名的函数实现。函数有一个实现签名,但是这个签名不能被直接调用。即使我们写了一个所需参数之后有两个可选的参数,但是也不能以两个参数调用
重载签名和实现签名
这是一个常见的混乱来源
1
2
3
4
5
6
7
8
9function fn(x: string): void;
function fn() {
// ...
}
// 期望以零参数调用
// 但是错误,提示未提供 x 变量
fn(); // error
fn(10); // 所以只能这样调用,那么这样使用重载签名的意义何在用于编写函数体的签名不能从外面看到
- 实现的签名从外面是看不到的,在编写重载函数时,应该总是在函数的实现上有两个或两个以上的重载签名
实现签名也必须与重载签名兼容
1
2
3
4
5
6
7
8
9
10function fn(x: boolean): void;
function fn(x: string): void; // error string boolean 不兼容 实现签名是 boolean 而 重载签名是 string 所以实现签名必须使用 联合类型才对
function fn(x: boolean) {}
function fn(x: string): string; // 返回类型不兼容
function fn(x: number): boolean;
function fn(x: number | string) {
return "ops";
}
编写好的重载
和泛型一样,在使用函数重载时,有一些准则需要遵循
- 在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function len(s: string): number;
function len(s: any[]): number;
function len(x: any) {
return x.length;
}
len("srt");
len([1, 2, 3]);
len(Math.random() > 0.5 ? "str" : [1, 2]); // error
// 这个函数的实现签名是兼容的
// 我们可以使用 字符串或数组来调用它
// 但是我们不能使用可能是字符串或数组的值来调用它,因为 ts 只能将一个函数调用解析为一个重载
// 因为两个重载都有相同的参数数量和相同的返回类型,可以改写为
function len(x: any[] | string) {
return x.length;
}
len("srt");
len([1, 2, 3]);
len(Math.random() > 0.5 ? "str" : [1, 2]);
函数内 This 的声明
TypeScript会通过代码流分析来推断函数中的this应该是什么1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38const user = {
id: 123,
admin: false,
becomeAdmin: function() {
this.admin = true;
}
}
// ts 理解函数 user.becomeAdmin 有一个对应的 this 它是外部对象 user,这个对于很多情况下已经足够了,但是在某些情况下,我们需要明确的指定函数中的 this 类型
// js 规范中,不能有一个叫 `this` 的参数,所以 TypeScript 使用这个语法空间,让你在函数体中声明 this 类型
interface User {
admin: boolean;
}
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db:DB = {
filterUsers: (filter: (this: User) => boolean) => {
let user1 = {
admin: true
}
let user2 = {
admin: false
}
return [user1, user2];
}
}
const admins = db.filterUsers(function(this: User) {
return this.admin;
});
// 但是注意,需要使用函数而不是箭头函数
const admins1 = db.filterUsers(() => this.admin); // error
需要了解的其他类型
- 一些函数的上下文中特别相关的类型
void
void表示没有返回值的函数的返回值。当一个函数没有任何返回语句,或者没有从这些返回语句中返回任何明确的值时,它都是推断出来的类型在
js中,一个不返回任何值的函数将隐含的返回undefined的值,然而在ts中,void和undefined是不一样的记住
void和undefined不一样1
2
3
4// 推断返回的类型是 void
function noop() {
return;
}
object
- 特殊类型
object指的是任何不是基元的值stringnumberbigintbooleansymbolnullundefined之外的。这和空对象类型{}不同,也与全局类型Object不同。你可能永远不会使用到Object object不是Object始终使用object- 注意:在
js中,函数是对象,它们有属性,在它们的原型链上有Object.prototype,是object的实例,可以对它们调用Object.keys等。由于这些原因,函数类型在Typescript中被认为是object
unknown
unknown类型代表任何值,这和any类型类似,但是更安全,因为对未知unknown值做任何事情都是不合法的- 这对于描述函数类型非常有用,因为你可以描述接受任何值的函数,而不需要在函数体有
any值。反之,你可以描述一个返回未知类型的值的函数
1 | function f1(a: any) { |
never
- 有些函数永远不会返回一个值
never类型表示永远不会被观察到的值,在一个返回类型中,这意味着函数抛出一个异常或终止程序的执行never也出现在TypeScript确定一个union中没有任何东西的时候
1 | function fail(msg: string): never { |
Function
全局性的 Function 类型描述了诸如 bind call apply 和其他存在于 JS 中所有函数值的属性。它还有一个特殊的属性。即 Function 类型的值总是可以被调用,这些调用返回 any
1 | // 这是一个无类型的函数调用,一般来说最好避免,因为 any 返回类型都不安全 |
参数展开运算符
形参展开
- 除了使用可选参数或重载来制作可以接受各种固定参数数量的函数外,还可以使用休止参数来定义接受无限制数量参数的函数
rest参数出现在所有其他参数之后,并使用...语法- 在
ts中,这些参数的类型注解是隐含的any[]而不是any,任何给出的类型注解必须是Array<T>或T[]的形式,或一个元组类型
1 | function mulitiply(n: number, ...m: number[]) { |
实参展开
反之,我们可以使用
spread语法从数组中提供可变数量的参数,例如,数组的push方法需要任意数量的参数1
2
3
4const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);请注意,一般来说
TS并不假定数组是不可变的,这可能会导致一些意想不到的行为
1 | const args = [8, 5]; |
参数解构
可以使用参数重构来方便的将作为参数提供的对象,解压到函数主体的一个或多个局部变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function sum({ a, b, c }) {
return a + b + c;
}
sum({ a: 10, b: 20, c: 30 });
// 对象的类型注解在解构的语法之后
function sum({ a, b, c }: { a: number; b: number; c: number }) {
return a + b + c;
}
type ABC = { a: number; b: number; c: number; };
function sum({ a, b, c }: ABC) {
return a + b + c;
}
函数的可分配性
返回 void 类型
- 函数的
void返回类型可以产生一些不同寻常的行为,但却是预期的行为 - 返回类型
void的上下文类型并不强迫函数不返回东西。另一种说法是,一个具有void返回类型的上下文函数类型 (type vf = () => void),在实现时,可以返回任何其他的值,但它会被忽略,因此,以下() => void类型的实现是有效的 - 需要注意,当一个字面的函数定义返回类型是
void,该函数不能返回任何东西
1 | type voidFunc = () => void; |
对象类型
在
js中,我们分组和传递数据的基本方式是通过对象,我们通过对象类型来表示这些对象对象类型可以是匿名的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
// 接口
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
// 或者类型别名
type Person = {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
属性修改器
- 对象类型中的每个属性都可以指定几件事: 类型、属性是否可选,以及属性是否可以被写入
可选属性
- 在属性后使用
?表示它是可选的
1 | type Shape = {}; |
在 js 中,即使该属性从未被设置过,我们任然可以访问它-它只是为定义的值
1 | function paintShape(opts: PaintOptions) { |
注意,这种未指定的值设置默认值非常普遍,以至于 js 有特殊的语法来支持它
1 | function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) { |
只读属性
对于
TS属性可以被标记为只读,虽然它不会在运行时改变任何行为,但是在类型检查期间,一个标记为只读的属性不能被写入1
2
3
4
5
6
7
8
9interface SomeType {
readonly prop: string;
}
function doSome(obj: SomeType) {
console.log(obj.prop);
obj.prop = "hello"; // error
}使用
readonly修饰符并不一定意味着一个值是完全不可改变的,或者换句话说,它的内部内容不能被改变,它只意味着该属性本身不能被重新写入1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18interface Home {
readonly resident: {
name: string;
age: number;
}
}
function visitForBirthday(home: Home) {
console.log("Happy ${home.resident.age} Birthday ${home.resident.name}");
home.resident.age++; // ok
}
function evict(home: Home) {
home.resident = {
name: "Victor the Evictor",
age: 42,
}; // error
}管理对
readonly含义的预期很重要,在ts开发中,对于一个对象应该如何被使用的问题,它是有用的信号。ts在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是否是readonly所以readonly属性也可以通过别名来改变1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writeblePerson: Person = {
name: "Person McPersonface",
age: 42
}
let readonlyPerson: ReadonlyPerson = writeblePerson;
console.log(readonlyPerson.age);
writeblePerson.age++;
console.log(readonlyPerson.age);
索引签名
有时并不提前知道一个类型的所有属性名称,但知道值的形状,在这种情况下,可以使用索引签名来描述可能的值的类型
1
2
3
4
5
6// [index: number] 索引属性,表示当一个 StringArray 被数字索引时,返回一个字符串
interface StringArray {
[index: number]: string;
}
let myArray: StringArray = ["1", "2"];索引签名的属性类型必须是
string或number支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型。这是因为当用
number进行索引时,js实际上会在索引到一个对象之前将其转换为string。这意味着用100进行索引和用'100'进行索引是一样的,所以两者需要一致1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
interface NotOkay {
[x: number]: Animal; // 错误 number 索引返回 Animal 而 string 索引返回 Dog, Animal 不是 Dog 的子类型
[x: string]: Dog;
}
interface YesKey {
[x: number]: Dog;
[x: string]: Animal;
}虽然字符串索引签名是描述字典模式的一种强大方式,但它也强制要求所有的属性与它们的返回类型想匹配。这是因为字符串索引声明
obj.proerty也可以作为obj["property"]在下面的例子中,name的类型与字符串索引的类型不匹配1
2
3
4
5interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string; // error 类型 string 的属性不能赋值给 string 索引类型 number
}然而,如果索引类型是属性类型的联合,不同类型的属性是可以接受的
1
2
3
4
5interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok
name: string; // ok
}最后,索引签名也可以为只读属性,以防止对其索引的赋值
1
2
3
4
5
6interface ReadonlyStringArray {
readonly [index: number]: string;
}
let array: ReadonlyStringArray = ["1", "2"];
array[2] = "3"; // error readonly
扩展类型
有些类型可能是其他类型的更具体的版本
1
2
3
4
5
6
7
8
9
10
11
12interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
// 现在需要一个地址的单元
interface AddressWithUnit extends BasicAddress {
unit: string;
}接口的
extends关键字,允许我们有效地从其他命名的类型中复制成员,并添加我们想要的任何新成员,这对于减少重复非常有用,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的接口也可以从多个类型中扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 200
}
交叉类型
接口允许通过扩展其他类型建立新的类型。而交叉类型可以通过组合其他类型建立新的类型
交叉类型用
&操作符定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
const cc: ColorfulCircle = {
color: "blue",
radius: 100
}
function draw(circle: ColorfulCircle) {
console.log(circle.color);
console.log(circle.radius);
}
draw({ color: "red", radius: 100 });
draw(cc);
接口与交叉类型
- 接口扩展和交叉类型非常相似,但实际上有些细小的不同,对于接口,我们可以使用
extends子句来扩展其他类型,而对于交叉类型,我们也可以做类似的事,并用类型别名来命名新类型。 - 两者之间的主要区别在于如何处理冲突,这种区别通常是在接口和交叉类型的类型别名之间选择的一个主要原因
- 接口 VS. 交叉类型
- 相同点
- 都可以描述对象或函数
- 都可以扩展其他类型
- 区别
- 不同的声明范围
- 接口: 声明中,值是具体结构对象
- 交叉: 可以为任意的类型创建类型别名
- 不同的扩展形式
- 接口: extends
- 交叉: &
- 不同的重复定义表现形式
- 接口: 自动合并
- 交叉: 报错
- 如何选择
- 建议优先选择接口
- 接口满足不了再使用交叉类型
1 | // 接口可以定义多次,多次的声明会自动合并 |
范型对象类型
如果有一个包含任何数据的盒子类型: 字符串、数字、苹果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72// any
interface Box {
contents: any; // any 但是ts的类型检查就失效了
}
// 或者使用 unknown
interface Box {
contents: unknown; // 虽然安全,但是使用时需要进行类型缩小
}
let box: Box = {
contents: "hello"
}
if (typeof box.contents === "string") {
console.log(box.contents.toUpperCase());
} else {
console.log(box.contents);
}
// 为每一个盒子搭建不同的类型
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
// 但这也意味着我们必须创建不同的函数或函数的重载,对这些结构类似的类型进行操作
function setContents(box: NumberBox, newContents: number): void;
function setContetns(box: StringBox, newContents: string): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
// 如果以后需要引入更多类型的盒子,这是非常差劲的
// 相反我们可以做一个通用的 Box 类型,声明一个类型参数
interface Box<T> {
contents: T;
}
let box1 = Box<string> = {
contents: "hello"
}
let box2 = Box<boolean> = {
contents: true
}
// 盒子是可重用的,因为 T 可以用任何东西来代替,这意味着当我们需要一个新类型的盒子时,我们根本不需要一个新的盒子类型
interface Apple {
weight: number;
}
let appleBox: Box<Apple> = {
contents: {
weight: 100
}
}
// 意味着我们也可以完全避免重载,而使用通用函数
function setContents<T>(box: Box<T>, contents: T) {
box.contents = contents;
}值得注意的是,类型别名也可以通用的,我们可以通过使用类型别名来代替
1
2
3type Box<T> = {
contents: T;
}由于类型别名与接口不同,它不仅可以描述对象类型,还可以用它来编写其他类型的通用辅助类型
1
2
3
4type OrNull<T> = T | null;
type OneOrMany<T> = Type | Type[];
type OneOrManyOrNull<T> = orNull<OneOrMany<T>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种形式工作是理想的,这样它们就可以在不同的数据类型中重复使用
数组类型
我们一直在使用这样一种类型: 数组类型,
number[]string[]这种类型 实际上是Array<number>和Array<string>的缩写1
2
3
4
5
6
7function doSome(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "hell"];
doSome(myArray);和上面的
Box类型一样,Array本身也是一个通用类型1
2
3
4
5
6interface Array<T> {
length: number;
pop(): T | undefined;
push(...items: T[]): number;
// ...
}现代
JS还提供了其他通用的数据结构,比如Map<K, V>Set<T>Promise<T>。这意味着,由于MapSetPromise的行为方式,它们可以与任何类型的集合一起工作
只读数组类型
ReadonlyArray是一个特殊的类型,描述了不应该被改变的数组1
2
3
4
5
6function doStuff(values: ReadonlyArray<string>) {
const copy = values.slice();
console.log(values[0]);
values.push("hello"); // error
}和属性的
readonly修饰符一样,它主要是我们用来了解意图的工具。当我们看到一个返回ReadonlyArray的函数时,我们知道我们不能改变它的内容。当我们看一个接受ReadonlyArray的函数的时候,我们可以将任何数组传入而不用担心它会改变其内容和
Array不同,没有一个我们可以使用的ReadonlyArray构造函数1
new ReadonlyArray("red"); // error
相反,我们可以将普通的
Array分配给ReadonlyArray1
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
正如
TypeScript为Array<T>提供了T[]的速记语法一样,它也为ReadonlyArray<T>提供了只读Type[]的速记语法一样1
2
3
4function doStuff(values: readonly string[]) {
const copy = values.slice();
values.push("hllo"); // error
}最后需要注意的是,与
readonly属性修改器不同,可分配性在普通Array和ReadonlyArray之间不是双向的1
2
3
4
5let x: readonly string[] = ["red", "green", "blue"];
let y: strng[] = [];
x = y; // 非readonly 可以分配给 readonly
y = x; // error readonly 不可以分配给非readonly
元组类型
Tuple类型是另一种Array类型,它确切地知道包含多少元素,以及它在特定位置包含哪些类型1
2
3
4
5
6
7// StringNumberPair 是一个 string number 的元组类型,和 ReadonlyArray 一样它在运行时没有任何表示,但对 ts 来说 它描述了其索引 0 包含字符串 和索引1 包含数字的数组
type StringNumberPair = [string, number];
// 如果我们试图索引超过元素的数量,我们会得到一个错误
function doSuff(pair: [string, number]) {
const c = pair[2];
}可以使用数组析构来对元组进行解构
1
2
3
4
5function doSuff(pair: [string, number]) {
let [str, num] = pair;
console.log(str);
console.log(num);
}除了长度检查,像这样的简单元组类型等同于
Array的版本,它为特定的索引声明属性,并且用数字字面类型声明长度1
2
3
4
5
6
7interface StringNumberPair {
length: 2;
0: string;
1: number;
// 其他 Array<string | number> 成员
slice(start?: number, end?: number): Array<string | number>
}元组可以通过在元素的类型后面写出问号
?---- 可选的元组,元素只能出现在末尾,而且还影响到长度的类型1
2
3
4
5
6type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
console.log(x, y, z);
}元组也可以有其余元素,这些元素必须是
array/tuple类型StringNumberBooleans描述了一个元组,其前两个元素分别是字符串和数字,但后面可以有任意数量的布尔StringBooleansNumber描述了一个元组,其第一个元素是字符串,最后一个元素是数字,中间可以是任意数量的布尔BooleansStringNumber描述了一个元组,其倒数第二个元素是字符串,最后一个元素是数字,起始位置可以是任意数量的布尔
1
2
3type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];ts允许将tuples与参数列表想对应。当想用一个其余参数接受可变数量的参数,并且需要一个最小的元素数量,但不想引人中间变量时,这很方便1
2
3
4
5
6
7
8function readButtonInput(...args: [string, number, ...boolean[]]) {
// ...
}
// 基本等同于
function readButtonInput(name: string, version: number, ...input: boolean[]) {
//...
}
只读元组类型
tuple类型有只读特性,可以通过在它们前面添加一个readonly关键字修饰符来指定1
2
3function doS(pair: readonly [string, number]) {
pair[0] = "hello"; // error 不可写入
}在大多数代码中,元组往往被创建并不被修改,所以在可能的情况下,将类型注释为一个字读元组是一个很好的默认。这一点很重要,因为带有
const的断言的数组字面量将被推断为只读元组类型1
2
3
4
5
6
7let point = [3, 4] as const;
function distance([x, y]: [number, number]) {
return Math.sqrt(x * x + y * y); // error 因为不能保证元组不被修改,所以错误
}
distance(point);
类型操纵
- 从类型中创建类型,
ts允许用其他类型的术语来表达类型 - 实际上有各种各样的类型操作符可以使用。也可以用我们已经有的值来表达类型
- 通过结合各种类型操作符,我们可以用一种简洁,可维护的方式来表达复杂的操作和值
- 泛型型 - 带参数的类型
Keyof类型操作符 -keyof操作符创建新类型Typeof类型操作符 - 使用typeof操作符来创建新的类型- 索引访问类型 - 使用
Type["a"]语法来访问一个类型的子集 - 条件类型 - 在类型系统中像
if语句一样行事的类型 - 映射类型 - 通过映射现有类型中的每个属性来创建类型
- 模版字面面类型 - 通过模版字面字符串改变属性的映射类型
泛型
- 在函数泛型已经介绍了泛型可以指定类型参数或者由
TS进行推断
通用类型变量
当我们使用泛型时,编译器会强制要求在函数主体中正确使用泛型参数,也就是说,实际上把这些参数当作是任何和所有的类型
1
2
3
4
5
6
7
8
9function identity<T>(arg: T): T {
console.log(arg.length); // error 不一定有 length 这个变量
return arg;
}
function identity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
泛型类型
泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似
1
2
3
4
5function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
泛型类
一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个 <> 中的泛型参数列表
1 | class GenericNumber<NumType> { |
和接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作
一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例而非静态侧具有通用性,所有在使用类时,静态成员不能使用类的类型参数
泛型约束
- 我们希望限制这个函数
any和所有类型一起工作,而不是与any和所有同时具有.length属性的类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。必须把我们的要求作为一个约束条件列在Type可以是什么
1 | interface Lengthwise { |
在泛型约束中使用类型参数
可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件
1 | function getProperty<T, K extends keyof T>(obj: T, key: K) { |
在泛型中使用类类型
在 TS 中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型
1 | function create<T>(c: { new (): T }): T { |
一个更高级的例子,使用原型属性来推断和约束类型的构造函数和实例之间的关系
1 | class Beekeeper { |
keyof 类型操作符
keyof 运算符接收一个对象类型,其产生其键的字符串或数字字面联合
1 | type Point = { x: number; y: number; }; |
如果该类型有一个字符串或数字索引签名,keyof 将返回这些类型
1 | type Arrayish = { [n: number]: unknown }; |
注意上面这个例子中,M 是 string | number – 这是因为 JavaScript 对象的键总是被强制为字符串,所以 obj[0] 总是和 obj['0'] 相同
keyof 类型在与映射类型结合时变得特别有用
Typeof 类型操作符
1 | let s = "hello"; |
typeof 对基本类型来说不是很有用,但结合其他类型操作符,可以使用 typeof 方便的表达许多模式。
这里介绍一个预定义的类型 ReturnType<T> 它接受一个函数类型并产生其返回类型
1 | type Predicate = (x: unknown) => boolean; |
ts 故意限制了可以使用 typeof 表达式的种类,具体来说,只有在标识符或其属性上使用 typeof 是合法的,这有助于避免混乱的陷阱
1 | let shouldContinue: typeof msgbox("helelo"); // error |
索引访问类型
我们可以使用索引访问类型来查询另一个类型上的特定属性
1 | type Person = { age: number; name: string; alive: boolean }; |
索引类型本身就是一个类型,所以我们可以完全使用 unions keyof 或其他类型
1 | interface Person { |
如果试图索引一个不存在的属性
1 | type I1 = Person["alve"]; // error |
另一种使用任意类型进行索引的例子是使用 number 来获取一个数组元素的类型,我们可以把它和 typeof 结合起来,方便地获取一个数组字面的元素类型
1 | const MyArray = [ |
只能在索引时使用类型,这意味着不能使用 const 来做一个变量引用
1 | const key = "age"; |
条件类型
在大多数有用的程序的核心,我们必须根据输入来做决定
JS也不例外,但鉴于数值可以很容易地被内省,这些决定也是基于输入的类型。条件类型有助于描述输入和输出的类型之间的关系1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// number
type Example1 = Dog extends Animal ? number : string;
// string
type Example2 = RegExp extends Animal ? number : string;
// 条件类型的形式看起来像是 js 中的条件表达式
SomeType extends OtherType ? TrueType : FalseType;
// 当 extends 左边的类型可以赋值给右边的类型时,那么你将得到第一个分支中的类型,否则得到后一个分支中的类型
// 当然上面这个例子看起来并不是显得有用,因为一眼就可以看出来它的类型。但条件类型的威力来自于它所带来的好处,条件类型的力量来自于它们与泛型一起使用
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}createLabel的这些重载描述了一个单一的js函数,该函数根据其输入的类型作出选择。注意一些事情:- 如果一个库必须在其
API中反复作出同样的选择,这就会变得很麻烦 - 必须创建三个重载:一个用于确定类型的情况,一个用于最一般的情况,对于
createLabel所能处理的每一种新类型,重载的数量都会呈指数级增长
1
2
3
4
5
6
7
8
9
10
11
12
13type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
// a: NameLabel
let b = createLabel(100);
// b: IdLabel
let c = createLabel(Math.random() > 0.5 ? "hello" : 42);
// c: NameLabel | IdLabel- 如果一个库必须在其
条件类型约束
条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型
1
2
3
4
5
6
7
8
9type Message<T> = T["message"]; // error 类型 message 无法用于索引类型 T
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;然而,如果我们想要让
MessageOf接受任何类型,并在消息属性不可用的情况下,默认为never类型呢,我们可以通过约束条件移出,并引入一个条件类型来做到这一点1
2
3
4
5
6
7type MessageOf<T> = T extends { message: unknown } ? T[message] : never;
type Dog = {
bark(): void;
}
type DogMessageContents = MessageOf<Dog>;
const dmc: DogMessageContents = "error" as never;在真正的分支中,
TS会知道T会有一个消息属性。下面也有一个例子,将数组类型平铺到它们的元素类型上,但在其他方面不做处理1
2
3
4
5
6
7
8// 当 Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取 string[] 的元素类型。否则,它只是返回它被赋予的类型
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>;
type Num = Flatten<number>;
let str1: Str = "123";
let num: Num = 123;
在条件类型内进行推理
我们只是发现自己使用条件类型来应用约束条件,然后提取出类型,这最终成为一种常见的操作,而条件类型使得它变得更容易
条件类型为我们提供了一种方法来推断在真实分支中使用
infer关键字进行对比的类型。例如,我们可以在Flatten中推断出元素类型,而不是用索引访问类型手动提取出来1
type Flatten<T> = T extends Array<infer Item> ? Item : T;
上面的例子,我们使用
infer关键字来声明性地引入一个名为Item的新的通用类型变量,而不是指定如何在真实分支中检索T的元素类型,这使得我们不必考虑如何挖掘和探测我们感兴趣的类型的结构我们可以使用
infer关键字编写一些有用的辅助类型别名,例如,对于简单的情况,我们可以从函数类型中提取出返回类型1
2
3
4
5
6
7type GetReturnType<T> = T extends (...args: never[]) => infer Return ? Return : never;
type Num = GetReturnType<() => number>;
type Str = GetReturnType<() => string>;
type Bool = GetReturnType<() => boolean>;
type Never = GetReturnType<string>;当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推测,这是最容许的万能情况),不可能根据参数类型的列表来执行重载解析
1
2
3
4
5declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>; // string | number
分布式条件类型
当条件类型作用于一个通用类型时,当给定一个联合类型时,它们就变成了分布式的。例如,以下面的例子为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22type ToArray<T> = T extends any ? T[] : never;
// 如果我们将一个联合类型插入 ToArray, 那么条件类型被用于该联合的每个成员
type StrArrOrNumber = ToArray<string | number>;
// string[] | number[] 这就是分布式 它的结果不是 (string | number)[]
/**
*
这里发生的情况是,StrArrOrNumArr 分布在
string | number;
并对每个联合的每个成员类型进行映射,以达到有效的目的
ToArray<string> | ToArray<number>
给我们留下了
string[] | number[]
通常情况下,分布性是需要的行为,为了避免这种行为,你可以使用方括号包围 extends 关键字的每一边
*/
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type StrArrOrNumArr = ToArrayNonDist<string | number>; // 现在是 (string | number)[]
映射类型
当不想重复定义类型,一个类型可以以另一个类型为基础创建新类型
映射类型建立在索引签名的语法上,索引签名用于声明没有被提前声明的属性类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26type OnlyBoolsAndNumber = {
[key: string]: boolean | number;
}
const confirms: OnlyBoolsAndNumber = {
del: true,
rodney: false
}
// 映射类型是一种通用类型,它使用 propertyKeys 的联合,经常通过 keyof 创建 迭代键来创建一个类型
type OptionsFlags<T> = {
[Property in keyof T]: boolean;
}
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
}
/**
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
type FeatureOptions = OptionsFlags<FeatureFlags>;
映射修改器
在映射的过程中,有两个额外的修饰可以应用:
readonly和?它们分别影响可变性和可选性可以通过
-或+作为前缀来删除或添加这些修饰语。如果不加前缀,那么假定是+1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22type CreateMutable<T> = {
-readonly [Property in keyof T]: T[Property];
}
type LockedAccount = {
readonly id: string;
readonly name: string;
}
type UnlockedAccount = CreateMutable<LockedAccount>;
type Concrete<T> = {
[Property in keyof T]-?: T[Property];
}
type MaybeUser = {
id: string;
name?: string;
age?: number;
}
type User = Concrete<MaybeUser>;
通过 as 做 key 重映射
可以通过映射类型中的
as子句重新映射映射类型中的键1
2
3
4// 语法
type MappedTypeWithNewProperties<T> = {
[Property in keyof T as NewKeyType]: T[Property];
}利用模版字面类型等功能,从先前的属性名称中创建一个新的属性名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// Capitalize ts提供的模版字符串类型 泛型传入 字符串类型 转换第一个字符大写其余部分不变
// string & Property 是为了确保 Property 是一个字符串类型 交集 暂时这么理解
type Getter<T> = {
[Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property];
}
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getter<Person>;
let lazyPerson: LazyPerson = {
getName: () => "suxi",
getAge: () => 18,
getLocation: () => "beijing"
}过滤掉某些属性
1
2
3
4
5
6
7
8
9// Exclude<U, V> U类型和V类型相同时返回null类型
type RemoveKindField<T> = {
[Property in keyof T as Exclude<Property, "kind">]: T[Property];
}
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;可以映射任意的联合体,不仅仅是
string | number | symbol的联合体,还可以是任何类型的联合体1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// in 操作符 E 类型 是否在联合类型 Events 内
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = {
kind: "square",
x: number,
y: number
}
type CircleEvent = {
kind: "circle",
radius: number
}
type Config = EventConfig<SquareEvent | CircleEvent>;映射类型与本类型操作部分的其他功能配合得很好,例如,这里有一个使用条件类型的映射类型,它根据一个对象的属性
pii是否被设置为字面意义上的true返回true或false1
2
3
4
5
6
7
8
9
10
11type ExtractPII<T> = {
[P in keyof T]: T[P] extends { pii: true } ? true : false;
}
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
}
// ObjectsNeedingGDPRDeletion 是 { id: false; name: true; }
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
类
类成员
类型属性的类型
在一个类上声明字段,创建一个公共的可写属性
1
2
3
4
5
6
7
8class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;和其他位置一样,类型注解是可选的,但如果不指定,将是一个隐含的
any类型字段也可以有初始化器,这些初始化器将在类被实例化时自动运行
1
2
3
4
5
6
7class Point {
x = 0;
y = 0;
}
const pt = new Point();
console.log(`${pt.x}, ${pt.y}`)和
constletvar一样,一个类属性的初始化器将被用来推断其类型1
2const pt = new Point();
pt.x = "0"; // errortsconfig.json中strictPropertyInitialization设置控制是否需要在构造函数中初始化实例字段1
2
3
4
5
6
7// 设置了 strictPropertyInitialization 为 true 需要在 构造函数中 初始化 字段
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}但是注意,字段需要在构造函数本身中初始化。
TypeScript不会分析从构造函数中调用的方法来检测初始化,因为派生类可能会覆盖这些方法而无法初始化成员如果打算通过构造函数以外的方式来确定初始化一个字段(例如,也许一个外部库为你填充了你的类的一部分),可以使用确定的赋值断言操作符
!1
2
3
4// 没有初始化,但没有报错
class OKGreeter {
name!: string;
}
readonly
字段的前缀可以是
readonly修饰符。这可以防止在构造函数之外对该字段进行赋值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Greeter {
readonly name: string = "world";
constructor(othername?: string) {
if (othername !== undefined) {
this.name = othername;
}
}
err() {
this.name = "not ok"; // error 不能在 constructor 以外的地方赋值
}
}
const g = new Greeter();
g.name = "also not ok" // error 不能在 constructor 以外的地方赋值
构造器
类构造函数与函数非常相似。可以添加带有类型注释的参数、默认值和重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Point {
x: number;
y: number;
// 带默认值的正常签名
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// ...
}
}类的构造函数签名和函数签名之间只有一些区别
- 构造函数不能有类型参数 - 这属于外层类的声明
- 构造函数不能有返回类型注释 - 类的实例总是被返回
super调用- 和
js一样,如果你有一个基类,在使用任何this.成员之前,需要在构造器主体中调用super
1
2
3
4
5
6
7
8
9
10
11
12class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// 在 ES5 中打印一个错误的值,在 ES6 中抛出异常
// 必须在访问派生类的 this 前调用 super
console.log(this.k);
super();
}
}- 和
方法
一个类上的函数属性被称为方法,方法可以使用函数和构造函数相同的所有类型注释
1
2
3
4
5
6
7
8
9class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}除了标准的类型注解,
ts并没有为方法添加其他新的东西注意,在一个方法体中,仍然必须通过
this访问字段和其他方法,方法体中的非限定名称将总是指代包围范围内的东西1
2
3
4
5
6
7
8let x: number = 0;
class C {
x: string = "hello";
m() {
x = "world"; // error 修改的是 第一行的 x
}
}
Getters / Setters
类也可以有访问器
1
2
3
4
5
6
7
8
9
10
11class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}注意,一个没有额外逻辑的字段支持的
get/set对在js中很少有用,如果不需要在get/set操作中添加额外的逻辑,暴露公共字段也是可以的TypeScript对访问器有一些特殊的推理规则- 如果存在
get但没有set,则该属性自动是只读的 - 如果没有指定
setter参数的类型,它将从getter的返回类型中推断出来 - 访问器和设置器必须有相同的成员可见性
- 如果存在
从
TypeScript4.3开始,可以有不同类型的访问器用于获取和设置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
索引签名
类可以声明索引签名;这些签名的作用与其他对象类型的索引签名相同
1 | class MyClass { |
因为索引签名类型需要同时捕获方法和属性,所以要有用地使用这些类型并不容易。一般来说,最好将索引数据存储在另一个地方,而不是类实例本身
类继承
implements 子句
可以使用一个
implements子句来检查一个类,是否满足一个特定的接口,如果一个类不能正确地实现它,就会发出一个错误类可以实现多个接口,例如
class C implements A, B {}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping");
}
}
class Ball implements Pingable {
// 缺少 ping 方法
pong() {
console.log("pong!");
}
}注意,
implements子句只是检查类是否可以被当作接口类型来对待,它根本不会改变类的类型或其方法。一个常见的错误来源是认为implements子句会改变类的类型 - 它不会! 它不会!1
2
3
4
5
6
7
8
9
10
11
12
13// 一个错误的例子
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
// s 隐式是 any 类型
// 我们可能期许它的类型会受到 check 的 name: string 参数的影响
// 但事实并非如此,实现子句并没有改变类主体的检查方式或其类型的推断
check(s) {
return s.toLowercase() === "ok";
}
}同样的,实现一个带有可选属性的接口并不能创建该属性
1
2
3
4
5
6
7
8
9
10
11interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10; // 类型C上不存在属性 y
extends 子句
类可以从基类中扩展出来,派生类拥有其基类的所有属性和方法,也可以定义额外的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Animal {
move() {
console.log("moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof");
}
}
}
const d = new Dog();
d.move();
d.woof(3);
重写方法
派生类也可以覆盖基类的一个字段或属性,可以使用
super.语法来访问基类方法。注意,因为JavaScript类是一个简单的查找对象,没有超级字段的概念TypeScript强制要求派生类总是其基类的一个子类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Base {
greet() {
console.log("hello world");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");派生类遵循其基类契约是很重要的,通过基类引用派生类实例是非常常见的(而且总是合法的)
1
2const b: Base = d;
b.greet();如果遵循基类的约定将会报错
1
2
3
4
5
6class Derived extends Base {
greet(name: string) {
// 必须的参数,报错
console.log("hello," + name.toUpperCase());
}
}
初始化顺序
- 类初始化的顺序是:
- 基类的字段被初始化
- 基类构造函数运行
- 派生类的字段被初始化
- 派生类构造函数运行
- 这个顺序意味着基类构造函数在自己的构造函数中看到自己的属性,因为派生类的字段初始化还没有运行
1 | class Base { |
继承内置类型
注意: 如果不打算继承 Array Error Map 等内置类型,或者编译目标明确设置为 es6/es2015 或以上,则不需要关注
在
es2015中,返回对象的构造函数隐含地替代了super(...)的任何调用者的this的值,生成的构造函数代码有必要捕获super(...)的任何潜在返回并将其替换为this因此,子类化
ErrorArray等可能不再像预期那样工作。这是由于ErrorArray等的构造函数使用ECMAScript6的new.target来调整原型链;然而,在ECMAScript 5中调用构造函数时,没有办法确保new.target的值。其他的下级编译器一般默认有同样的限制对于下面的子类 有以下问题
- 方法在构造这些子类所返回的对象上可能是未定义的,所以调用
sayHello会导致错误 instanceof将在子类的实例和它们的实例之间被打破,所以new MsgError() instanceof MsgError将返回false
1
2
3
4
5
6
7
8
9
10
11
12class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
const msgError = new MsgError("error");
console.log(msgError.sayHello()); // TypeError msgError.sayHello is not a function- 作为建议,可以在任何
super调用后立即手动调整原型
1
2
3
4
5
6
7
8
9
10class MsgError extends Error {
constructor(m: string) {
super(m);
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
cosnole.log("hello")
}
}- 方法在构造这些子类所返回的对象上可能是未定义的,所以调用
成员的可见性
public
- 类成员的默认可见性是公共的,一个公共成员可以在任何地方被访问
public是默认的可见性修饰符,所以永远不需要在类成员上书写它,除非为了风格/可读性的原因
1 | class Greeter { |
protected
受保护的
protected成员只对它们所声明的类或子类可见1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Greeter {
public greet() {
console.log("hello " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
console.log("howdy " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet();
g.getName(); // 无权访问受保护成员的暴露
- 派生类需要遵循它们的基类契约,但可以选择公开具有更多能力的基类的子类型,这包括将受保护的成员变成公开
1
2
3
4
5
6
7
8
9
10class Base {
protected m = 10;
}
class Derived extends Base {
m = 15;
}
const d = new Derived();
console.log(d.m); // 没有问题
private
private和protected一样,但不允许从子类中访问该成员1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Base {
private x = 0;
getX() {
return this.x;
}
}
const b = new Base();
console.log(b.x); // 不能类外访问
class Dervied extends Base {
showX() {
console.log(this.x); // 不能在子类访问
}
}私有成员对于派生类是不可见的,所以派生类不能增加其可见性
跨实例的私有访问
TS允许跨实例私有访问1
2
3
4
5
6
7class A {
private x = 10;
public sameAs(other: A) {
return other.x === this.x;
}
}像
TS类型系统的其他方面一样,private和protected只在类型检查中被强制执行这意味着
JS的运行时结构,如in或简单的属性查询,仍然可以访问一个私有或保护的成员private也允许在类型检查时使用括号符号进行访问,这使得私有声明的字段更容易被单元测试之类的东西所访问,缺点是这些字段是软性私有的,不能严格执行私有特性1
2
3
4
5
6
7
8class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
console.log(s.secretKey); // error
console.log(s["secretKey"]); // ok与
TS的private不同,JS的private字段#在编译后仍然是private的,并且不提供前面提到的像括号符号访问那样的窗口,使其成为private1
2
3
4
5
6
7
8
9
10
11class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {
console.log(this.#barkAmount);
}
}
let dog = new Dog();
console.log(dog.#barkAmount); // error
console.log(dog["#barkAmount"]) // error // 即便编译成功也是 undefined
静态成员
- 类可以有静态成员,这些成员并不与类的特定实例相关联。它们可以通过类的构造函数对象本身来访问
- 静态成员也可以使用相同的
publicprotected和private可见性修饰符 - 静态成员也会被继承
- 和
JS一样,静态方法中的this代表类本身
1 | class MyClass { |
特殊的静态名称
- 一般来说,从函数原型覆盖属性是不安全/不可能的,因为类本身就是可以用
new调用函数,所以某些静态名称不能使用。像namelength和call这样的函数属性,定义为静态成员是无效的
1 | class S { |
类里的 static 区块
- 静态块允许写一串有自己作用域的语句,可以访问包含类中的私有字段,这意味着我们可以用写语句的所有能力来写初始化代码,不泄露变量,并能完全访问我们类的内部结构
1 | class Foo { |
泛型类
类,和接口一样,可以是泛型的。当一个泛型类用 new 实例化时,其类型参数的推断方式与函数调用的方式相同
1 | class Box<T> { |
- 类可以像接口一样使用通用约束和默认值
- 静态成员不能引用类的类型参数,因为类型总是被完全擦除的
- 一个泛型类的静态成员永远不能引用该类的类型参数
类运行时的 this
记住,
TypeScript并没有改变JavaScript的运行时行为,而JavaScript的运行时行为偶尔很奇怪比如,
JavaScript对这一点的处理确实是不寻常的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName
}
console.log(obj.getName()); // 输出 obj 而不是 MyClass默认情况下,函数内
this的值取决于函数的调用方式,在这个例子中,因为函数是通过obj引用调用,所以它的this值是obj而不是类实例,这很少是希望发生的事情,TS提供了一些方法来减轻或防止这种错误箭头函数
- 如果有一个经常被调用的函数,失去它的
this上下文,那么使用一个箭头函数而不是方法定义是有意义的
1
2
3
4
5
6
7
8
9
10
11
12class MyClass {
name = "MyClass";
getName = () => {
return this.name;
}
}
const c = new MyClass();
const g = c.getName;
console.log(g()); // myClass- 如果有一个经常被调用的函数,失去它的
关于箭头函数,还有一些权衡
this值保证在运行时是正确的,即使是没有经过TypeScript检查的代码也是如此- 这将使用更多的内存,因为每个类实例都将有它自己的副本,每个函数都是这样定义的
- 不能在派生类中使用
super.getName,因为在原型链中没有入口可以获取基类方法
this参数出现在所有其他参数之后- 在方法或函数定义中,一个名为
this的初始化参数在TypeScript中具有特殊的意义。这些参数在编译过程中会被删除
1
2
3
4
5
6
7function fn(this: SomeType, x: number) {
//
}
// 编译后
function fn(x: number) {
}TS检查调用带有this参数的函数,是否正确的上下文中进行,我们可以不使用箭头函数,而是在定义方法中添加一个this参数,以静态地确保方法被正确调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
c.getName(); // 正确
const g = c.getName;
console.log(g());- 这种方法做出了与箭头函数方法相反的取舍
JS调用者仍然可能在不知不觉中错误地使用类方法- 每个类定义只有一个函数被分配,而不是每个类实例一个函数
- 基类方法定义仍然可以通过
super调用
- 在方法或函数定义中,一个名为
this 类型
在类中,一个叫做
this的特殊类型动态地指向当前类的类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Box {
contents: string = "";
set(value: string) {
this.contents = value;
// 在这里推断出 set 返回的类型是 this,而不是Box
return this;
}
}
class ClearbleBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearbleBox();
const b = a.set("hello");
console.log(b);也可以在参数类型注释中使用
this1
2
3
4
5
6
7
8
9
10class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
const box = new Box();
console.log(box.sameAs(box))这与其他写法不同:
Box如果有一个派生类,它的sameAs方法现在只能接受该同一派生类或其派生类的子类的其他实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
}
const base = new Box();
const derived = new DerivedBox();
base.sameAs(derived); // 没有问题
derived.sameAs(base); // error 只能接受 派生类 DerivedBox及其子类 类型的实例
基于类型守卫的 this
可以在类和接口的方法返回位置使用
this is Type。当与类型缩小混合时(例如if语句),目标对象的类型将被缩小到指定的Type1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {
}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar", "baz");
const di: FileSystemObject = new Directory("foo/bar", false);
const fs: FileSystemObject = new FileSystemObject("foo/bar", true);
function switch1(fso) {
if (fso.isFile()) {
console.log(fso.content);
} else if (fso.isDirectory()) {
console.log(fso.path);
} else if (fso.isNetworked()) {
console.log(fso.host);
}
}
switch1(fso);
switch1(di);
switch1(fs);基于
this的类型保护的一个常见用例,是允许对一个特定字段进行懒惰验证。例如,下面这种情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box<string>();
box.value = "GameBoy";
// box.value: string | undefined
if (box.hasValue()) {
// 这里已经可以推断出 box.value: string
console.log(box.value.toUpperCase());
}
参数属性
TS提供了特殊的语法,可以将构造函数变成具有相同名称和值的类属性。这些被称为参数属性,通过在构造函数参数前加上可见性修饰符publicprivateprotected或readony中的一个来创建。由此产生的字段会得到这些修饰符
1 | class Params { |
类表达式
- 类表达式与类声明非常相似。唯一真正的区别是,类表达式不需要一个名字,尽管我们可以通过它们最终绑定的任何标识符来引用它们
1 | const someClass = class<T> { |
抽象类和成员
TypeScript中的类、方法和字段可以是抽象的。一个抽象的方法或抽象的字段是一个没有提供实现的方法或字段。这些成员必须存在于一个抽象类中,不能直接实例化抽象类的作用是作为子类的基类,实现所有的抽象成员。当一类没有任何抽象成员时,我们就说它是具体的
1
2
3
4
5
6
7
8abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
let b = new Base(); // error不能用
new关键字来实例化Base因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员1
2
3
4
5
6
7
8class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();抽象构造签名
- 如果想接受一些类的构造函数,产生一个从某些抽象类派生出来的类的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// TypeScript 正确地告诉你,你正在试图实例化一个抽象类。毕竟鉴于 greet 的定义,写这段代码是完全合法的,它最终会构造一个抽象类
function greet(ctor: typeof Base) {
const instance = new ctor(); // error 不能创建一个抽象类
instance.printName();
}
// 糟糕
greet(Base);
// 相反,写一个函数,接受具有结构化签名的东西
function greet(ctor: new() => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base); // error
类之间的关系
在大多数情况下,
TypeScript中的类在结构上与其他类型相同,是可以比较的例如,这两个类可以相互替代使用,因为它们是相同的
1
2
3
4
5
6
7
8
9
10
11class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
const p: Point1 = new Point2(); // 正确的同样的,即使没有明确的继承,类之间的子类型关系也是存在的
1
2
3
4
5
6
7
8
9
10
11
12class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
const p: Person = new Employee(); // right这看起来很简单,但是有几种情况似乎比其他情况更奇怪。空的类没有成员,在一个结构化类型系统中,一个没有成员的类型通常是其它任何东西的超类型。所以如果你有一个空类,任何东西都可以用来代替它
1
2
3
4
5
6
7
8
9
10
11
12class Empty {
}
function fn(x: Empty) {
// 不能用 x 做任何事
}
fn(window);
fn({});
fn(fn);
模块
如何定义 JavaScript 模块
- 在
TypeScript中,就像在ECMAScript2015中一样,任何包含顶级import或export的文件都被认为是模块 - 相反,一个没有任何顶级导入或导出声明的文件被视为一个脚本,其内容可在全局范围内使用(因此也可用于模块,例如
import "./index.js") - 模块在自己的范围内执行,而不是在全局范围。这意味着在模块中声明的变量、函数、类等在模块外是不可见的,除非它们被明确地用某种导出形式导出。相反,要使用从不同模块导出的变量、函数、类、接口等,必须使用导入的形式将其导入
非模块
JavaScript规范声明,任何没有export或顶层await的JavaScript文件都应该被认为是一个脚本而不是一个模块在一个脚本文件中,变量和类型被声明为在共享全局范围内,并且假定会使用
outFile编译器选项将多个输入文件加入一个输出文件,或者在你的html中使用script标签来加载这些文件如果有一个目前没有任何导入或导出的文件,但希望被当作一个模块来处理,请添加这一行:
1
export {}
这将改变该文件,使其成为一个什么都不输出的模块。无论你的模块目标是什么,这个语法都有效
TypeScript 中的模块
- 在
TypeScript中编写基于模块的代码时,有三个主要方面- 语法
- 模块解析
- 模块输出目标
ES 模块语法
一个文件可以通过
export default声明一个主要出口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// hello.ts
export default function helloworld() {
console.log("hello world");
}
// 导入
import hello from "./hello.ts";
hello();
// 除了默认的导出,还可以通过省略 default 的 export 实现一个以上的变量和函数的导出
// math.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
// 导入
import { pi, phi } from "./math.ts"
console.log(pi);
额外的导入语法
可以使用
import { old as new }这样的格式来重命名一个导入1
2
3import { pi as PI } from "./math.ts";
console.log(PI);可以把所有导出的对象,用
* as name放到一个命名空间1
2
3import * as math from "./math.ts";
console.log(math.pi);也可以通过
import "./file"导入一个文件,而不把任何变量纳入你的当前模块。这种情况下,import没有任何作用,然后math.ts中的所有代码都被解析了,这可能引发影响其他对象的副作用1
2import "./math.ts";
console.log(3.14)
TypeScript 特定的 ES 模块语法
类型可以使用与
JS值相同的语法进行导出和导入1
2
3
4
5
6
7
8
9export type Cat = { breed: string; yearOfBirth: number; };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
import { Cat, Dog } from "./animal.ts";
type Animals = Cat | Dog;TS用了两个概念扩展了import语法,用于声明一个类型的导入import type
1
2
3
4
5
6
7
8export type Cat = { breed: string; yearOfBirth: number; };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
import type { Cat, Dog } from "./animal.ts";内联类型导入
TS4.5还允许以type为前缀的单个导入,以表明导入的引用是一个类型
1
2
3import { type Cat, type Dog } from "./animal.ts";
export type Animals = Cat | Dog;
ES 模块语法与 CommonJS 行为
TS 有 ES Module 语法,它直接与 CommonJS 和 AMD 的 require 想关联。使用 ES Module 的 import 在大多数情况下与这些环境的 require 相同,但这种语法确保你在 TypeScript 文件中与 CommonJS 的输出有1对1的匹配
1 | import fs = require("fs"); |
CommonJS 语法
CommonJS 是 npm 上大多数模块的交付格式
导出
标识符是通过一个全局调用的 module 上设置 exports 属性来导出的
1 | function absolute(num: number) { |
这些文件可以通过 require 语句导入
1 | const maths = require("maths"); |
CommonJS 和 ES 模块的互操作性
- 关于默认导入和模块命名空间对象导入之间的区别,
CommonJS和ES Modules之间存在功能上的不匹配
TypeScript 的模块解析选项
- 模块解析是指从
import或require语句中获取一个字符串,并确定该字符串所指的文件的过程 TS包括两种解析策略,经典和Node。当编译器选项module不是commonjs时,经典策略是默认的,是为了向后兼容。Node策略复制了Node在CommonJS模式下的工作方式,对.ts和.d.ts有额外的检查- 在
TS中,有许多TSConfig标志影响模块策略moduleResolutionbaseUrlpathsrootDirs
TS 的模块输出选项
有两个选项会影响
JS输出target它决定了哪些JS功能被降级(转换在旧 js 运行时运行),哪些保持不变module它决定了哪些代码用于模块之间的相互作用
使用的
target由你期望运行TypeScript代码的JavaScript运行时中的可用功能决定的。这可能是:支持的最古老的网络浏览器,期望运行的最低版本的NodeJS,或者可能来自于运行时的独特约束 — electron所有模块之间的通信都是通过模块加载器进行的,编译器选项
module决定使用哪一个。在运行时,模块加载器负责在执行一个模块之前定位和执行该模块的所有依赖项例如,这里是一个使用
ES模块语法的TypeScript文件,展示了module的一些不同选项1
2import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;- ES2020
1
2import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;- CommonJS
1
2
3
4
5;
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;UMD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./constants.js"], factory);
}
})(function (require, exports) {
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;
});
TS 命名空间
TS 有自己的模块格式,称为 命名空间,这比 ES 模块标准要早。这种语法对于创建复杂的定义文件有很多有用的功能,并且在 DefinitelyTyped 中仍然被积极使用。虽然没有被废弃,但命名空间中的大部分功能都存在于 ES Modules 中,我们建议使用与 Js 的方向保持一致
