TypeScript 入门

TSC 编译器

1
npm i -g typescript
1
2
// hello.ts
console.log('Hello World')
1
tsc hello.ts

发出错误

  1. --noEmitOnError 不允许发生错误,一旦发生错误将不会生成 js 文件
1
tsc --noEmitOnError hello.ts

显式类型

  1. 明确指定参数或变量的类型
  2. 但是我们不总是需要编写明确的类型,在大多数情况下,TypeScript 会自动推断代码中的类型
1
2
3
4
5
6
function greet(person: string, data: Date) {
console.log(person);
console.log(data);
}

greet("suxi", new Date());

擦拭类型

  1. tsc 编译后的 js 将类型擦除掉
  2. 类型注释永远不会改变程序的运行时行为

降级编译

  1. tsc 指定编译生成 js 的版本
  2. tsc --target es2015 hello.ts

严格模式

  1. tsc --strict true hello.ts 开启严格模式
  2. tsc --strict true --noImplictAny true 开启严格模式,并且当类型隐式推断为 any 时发出错误
  3. tsc --target es2015 --strictNullChecks true null 和 undefined 可以分配给任意类型,这可能会导致空异常,开启 strictNullChecks 可以防止出现空异常

常用类型

typescript

1
tsc --init
1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
"target": "es2016",
"rootDir": "./src",
"outDir": "./dist",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

基元类型 number string boolean

1
2
3
let a: number = 123;
let s: string = "abs";
let b: boolean = true;

数组

1
2
let arr: number[] = [1, 2, 3];
let strs: Array<string> = ["abc", "def"];

any

  1. 当不希望某个特定值导致类型检查错误时,可以使用 any
  2. 当一个值的类型是 any 时,可以访问它的任何属性,将它分配给任何类型的值
  3. 但在运行环境下执行代码可能是错误的
  4. 当我们不指定类型时,并且 typescript 无法从上下文推断它时,编译器通常会默认为 any,但是通常情况下,我们需要避免这种情况,因为 any 没有进行类型检查,使用 noImplicitAny 将任何隐式标记 any 为错误
1
2
3
4
5
6
7
8
9
let obj: any = {
x: 1,
y: 1
}

obj.foo();
obj.bar = 1000;
const n: number = obj;
obj = null;

变量上的类型注释

  1. 可以选择添加类型注释来显式指定变量的类型
  2. 但是这不是必须的,因为 typescript 会尝试自动推断代码中的类型
1
2
let myName: string = "suxi";
let name: string = "peiqi";

函数

参数类型注释

  1. 即便没有参数类型注释,仍然会检查参数的数量
1
2
3
4
5
6
function greet(name: string) {
console.log(`Hello ${name}`);
}
greet(42); // error
greet("123", "456"); // error
greet("42");

返回类型注释

  1. 通常不需要返回类型,因为 typescript 会推断出返回类型
1
2
3
function getNumber(): number {
return 123;
}

匿名函数

  1. 匿名函数与函数声明有所不同,当一个函数出现在 typescript 可以确定它将如何被调用的地方时,该函数的参数会自动指定类型
  2. 上下文推断类型,函数发生在其中的上下文通知它应该具有什么类型
1
2
3
const names: Array<string> = ["abc", "def"];
// 自动推断 name 为 string
names.forEach(name => console.log(name.toUpperCase()));

对象类型

  1. 下面是一个对象类型的例子
  2. x 和 y 是对象的属性,它们的类型为 number
  3. 可以使用 , 或 ; 分隔属性,最后一个分割符是可选的
  4. 每个属性的类型部分也是可选的,如果不指定类型,则将假定为 any
1
2
3
4
let point: { x: number; y: number; } = {
x: 1,
y: 1
}

可选属性

  1. 对象类型还可以指定其部分或全部属性是可选的
  2. 在属性后添加 ? 表示其可选属性
  3. 当使用可选属性时,首先要判断是否存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let point: { x: number; y: number; z?: number } = {
x: 1,
y: 1
}

if (point.z !== undefined) {
console.log("三维坐标");
} else {
console.log("二维坐标");
}

point = {
x: 1,
y: 1,
z: 1
}

联合类型

typescript 允许使用多种运算符从现有类型中构建新类型

  1. 定义联合类型

    • 联合类型是由多个其他类型组成的类型,表示可以是这些类型中的任何一种类型
    • 这些类型中的每一种类型称为联合类型的成员
    1
    2
    3
    4
    5
    function print(id: number | string) {
    console.log(id);
    }
    print(123);
    print("123");
  2. 使用联合类型

    • 提供联合类型很容易,但是使用时,如果联合的每个成员都有效,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. 如果我们想要多次使用一个类型,恰好这个类型的定义又很复杂,我们可以使用类型别名来声明类型,以便于多次引用它
  2. 当然类型别名只是别名,不能使用类型别名来创建相同类型的不同版本
1
2
3
4
5
6
7
8
9
10
11
12
type Point = {
x: number;
y: number;
z?: number;
}

let point: Point = {
x: 1,
y: 1
}

type ID = string | number;

接口

  1. 接口声明是另一种方式来命名对象类型
  2. ts 只关注类型的结构和功能
1
2
3
4
5
6
7
8
9
10
11
interface Point {
x: number;
y: number;
z?: number;
}

let point: Point = {
x: 1,
y: 1,
z: 1
}

接口和类型别名之间的差异

  1. 类型别名和接口非常相似,在多数情况下可以自由的选择它们,几乎所有功能都在 interface 中可用 type 关键区别在于扩展新类型的方式不同
  2. 类型别名可能不参与声明合并,但接口可以
  3. 接口只能用于声明对象的形状,不能重命名基元
  4. 接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 扩展接口
interface Animal {
name: string;
}

interface Bear extends Animal {
honey: boolean;
}

const bear: Bear = {
name: "winnie",
honey: true
}

bear.name;
bear.honey;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 通过交叉点扩展类型
type Animal = {
name: string;
}

type Bear = Animal & {
honey: boolean;
}

const bear: Bear = {
name: "winnie",
honey: true
};

bear.name;
bear.honey;
1
2
3
4
5
6
7
8
9
10
11
12
13
// 向现有类型添加新字段
interface MyWindow {
title: string;
}

interface MyWindow {
content: string;
}

const w: MyWindow = {
title: "hello ts",
content: "ts is pop"
}
1
2
3
4
5
6
7
8
// 类型创建后不可更改
type MyWindow = {
title: string;
}

type MyWindow = {
content: string;
}

类型断言

  1. 有时,我们会获得有关 TS 不知道的值类型的信息,例如 document.getElementById , TS 只知道它将返回某种类型的 HTMLElement 但我们自己知道它将始终返回 HTMLCanvasElement 类型与穷尽性检查,这种情况下,我们需要类型断言来指定更加具体的类型

    1
    const canvas = document.getElementById("canvas") as HTMLCanvasElement;
  2. 和类型注释一样,类型断言不会影响代码的运行行为并由编译器删除断言,当然也可以使用 <> 进行断言,它们是等效的,但是需要注意这种方式不可以用在 tsx 中

    1
    const canvas = <HTMLCanvasElement>document.getElementById("canvas");
  3. TS 只允许断言转换为更具体或不太具体的类型版本,此规则可防止不可能的断言

    1
    2
    3
    4
    // 不可能的断言 因为这两种类型没有充分重叠
    const x = "hello" as number;
    // 需要先断言为 unknown 或 any 类型 在断言为 number 类型
    const (x = "hello" as unknown) as number;

文字类型

  1. 除了一般类型 string number,我们还可以在类型位置引用特定的字符串和数字

  2. 一种方法是考虑 js 如何以不同的方式声明变量

    • var let 两者都允许更改变量中保存的内容
    • const 不允许,这反映在 ts 如何为文字创建类型上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let testString = "hello world";
    testString = "123"; // 可以任意更改 相当于 testString的类型是 string

    const constantString = "hello world";
    // 已经不能在更改了,它只能表示一个可能的字符串,所有实际上 constantString 的类型是 "hello world" 这就是文字类型

    // 但就其本身而言,文字类型并不是很有价值
    let x: "hello" = "hello";
    x = "hello"; // 正确
    x = "xxxx"; // 错误
  3. 拥有一个只能有一个值的变量并没有多大用处,但是通过将文字组合成联合类型,可以用来表达一个更有用的概念 — 例如只接受一组特定的值

  4. 当然文字类型也可以和非文字类型结合使用

  5. 还有一种文字类型:布尔文字,只有两种布尔文字类型,它们是类型 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
    23
    function 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);

文字推理

  1. 当我们使用对象初始化变量时,TS 假定该对象的属性稍后可能会更改值

    • 所以 const 定义对象时,并不认为 counter 就是一个文字类型,而是认为它是一个基元 number,所以我们改变 counter 的值,ts 并不认为它是错误的
    • 这也符合 js 中的行为
    1
    2
    3
    4
    const obj = { counter: 0 }; // 注意对象 const counter 属性可能改变
    obj.counter = 1;

    obj.counter = "number"; // 错误
  2. 上面的行为同样适用于字符串

  3. 可以使用 as const 将整个对象转换为类型文字,就是说,确保了对象的所有属性分配的都是文字类型,而不是一个更一般的 string number 等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function 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

  1. js 中 null 和 undefined 表示不存在或未初始化的值

  2. ts 中有两个对应的同名类型,这些类型的行为取决于是否设置了 strictNullChecks 选项

    • 关闭 false 依然可以正常访问的值,并且可以将值分配给任何类型的属性。这类似没有空检查的语言 (c# java),缺少空检查往往是错误的主要来源
    • 开启,需要在对值使用之前测试这些值,就像在使用可选属性之前检查一样,我们需要使用缩小来检查可能的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function 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());
    }
    }
  3. 非空断言运算符 (! 后缀)

    • ts 的特殊语法,可以在不进行任何显式检查下,使用值
    • ! 在任何表达式之后写入实际上是一种类型断言,表示该值 我知道它由于某种原因它不是 null 或 undefined
    • 就像类型断言一样,它不会更改代码的运行时行为,因此仅当你知道它不是 null 或 undefined 时才应该使用非空断言运算符,这才是重要的
    1
    2
    3
    function liveDangerous(x?: number | null) {
    console.log(x!.toFixed(2)); // 我知道 x 是 number 类型
    }

枚举

  1. 枚举是 ts 添加到 js 中的一个功能,它允许描述一个值,该值是一组可能的命名常量之一。
  2. 关于枚举更深层次的使用
1
2
3
4
5
6
7
8
9
enum Direction {
Up = 1,
Down,
Left,
Right,
}

console.log(Direction.Up); // 1
console.log(Direction.Down); // 2
1
2
3
4
5
6
7
8
9
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
console.log(Direction.Down);

不太常见的原语

bigint

  1. 从 es2020 开始,js 中有一个用来表示非常大的整数的原语 BigInt
1
2
let bigNumber1: bigint = BigInt(100);
let bigNumber2: bigint = 100n;

symbol

  1. Symbol 用来通过函数创建全局唯一引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let sy1: symbol = Symbol("foo");
let sy2: symbol = Symbol("foo");

let sy3 = Symbol("ooo");
let sy4 = Symbol("ooo");

const sy5 = Symbol("111");
const sy6 = Symbol("222");

if (sy1 === sy2) {
// 虽然这里也永远无法执行 但它不会报错
console.log(11111);
}

if (sy3 === sy4) {
// 虽然这里也永远无法执行 但它不会报错
console.log(22222);
}

if (sy5 === sy6) { // error
// 永远不会执行,因为此条件始终返回 `false` 因为类型 typeof sy1 和 typeof sy2 没有重叠
// 这是不是和文字类型比较类似了
}

类型缩小

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
function padLeft(padding: number | string, input: string) {
reurn new Array(padding + 1).join(" ") + input; // error
// 运算符 + 不能应用于类型 string | number
// 我们没有明确检查 padding 是否为 `number` 也没有处理它是 string 的情况
}


// 使用类型缩小
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
} else {
return padding + input;
}
}

/**
这看起来和无聊的 JS 一样,但是 `TS` 的类型主旨在于编写典型的 `JS` 代码尽可能容易,而不需要弯腰去获取类型安全

TS 使用静态类型分析运行的值一样,它在 JS 的运行时控制流构造上构造叠加了类型分析
if else 三元运算 循环 真值 等 这些都会影响到这些类型

在使用 typeof padding === "number" 时可以理解为一种特殊形式的代码,称为类型保护

TS 遵循程序可能采取的执行路径,来分析一个值在特定位置的最具体的可能类型
它查看这些特殊的检查和赋值,将类型细化为比声明更具体的类型的过程称为缩小

那么主要有以下几种缩小
*/

typeof 类型守卫

  1. typeof 和 js 运算符一样,它可以提供有关我们运行时拥有的值类型的非常基本的信息。ts 期望它返回一组特定的字符串
    • string
    • number
    • bigint
    • boolean
    • symbol
    • undefined
    • object
    • function
  2. 在 TS 使用 typeof 可以理解为,它缩小在不同分支中的类型
  3. 在 typescript 中检查 typeof 的返回值是一种类型保护
  4. 注意 typeof 不返回字符串 null
1
2
3
4
5
6
7
8
9
10
11
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) { // error 因为 它可能是 null
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do something
}
}

真值缩小

  1. 真值检查通常在 js 我们也会这样做: && || if ! 等表达式

    1
    2
    3
    4
    5
    6
    7
    function getPersonCount(count: number) {
    if (count) {
    console.log(`There are ${count} people`);
    } else {
    console.log("There are no people");
    }
    }
  2. 这样通过 if 语句将它们的条件强制转化为 boolean 使它有意义,然后根据结果是 true false 来选择它们的分支

  3. 下面这些值将会强制转换为 false。其他值被转换为 true,你始终可以在 Boolean 函数中运行值获得 boolean 或使用较短的双布尔否定将值强制转换为 boolean,当然双重否定的优点在于 ts 将它推断为一个 true 的文字类型 比较狭窄 而 Boolean 是一个 boolean 类型

    • 0
    • NaN
    • ""
    • 0n
    • null
    • undefined
    1
    2
    Boolean("hello"); // boolean
    !!"1"; // true 类型
  4. 利用真值缩小可以防范于 null undefined 之类的值的影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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
    }
    }
  5. 但是对原语的真值检查通常容易出错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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. 使用 === !== != == 等值检查来实现类型缩小,叫等值缩小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function 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);
    }
    }
  2. 在真值缩小中我们使用了一个不完善的缩小从而将空字符串从其中排除掉了,那么我们可以使用等值缩小进行完善

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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);
    }
    }
    }
  3. js 更宽松的相等性检查 == != 也能正确缩小。如果要检查一个变量是否等于 null 或 undefined 那么使用 != 或 == 是一个好的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface Container {
    value: number | null | undefined;
    }

    function multiplyValue(container: Container, factor: number) {
    if (container != null) {
    console.log(container.value);
    container.value *= factor;
    }
    }

in 操作符缩小

  1. in 运算符,用于确定对象是否拥有某个名称的属性

  2. value in x value 是字符串文字,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
    22
    type 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 操作符缩小

  1. instanceof 检查一个值是否是另一个值的实例。更具体的 x instanceof Foo 检查 x 的原型链中是否含有 Foo.prototype
1
2
3
4
5
6
7
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}

分配缩小

  1. 我们在为任何变量赋值时,TypeScript 会查看赋值的右侧并适当缩小左侧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let x = Math.random() < 0.5 ? 10 : "hello world"; // string | number

    x = 1;
    console.log(x);

    x = "goodbye";
    console.log(x);

    x = false // error
  2. 注意,这些分配中的每一个都是有效的,即使在我们第一次赋值后观察到 x 更改为 number 我们仍然可以将 string 赋值给 x ,这是因为 x 在声明时是 string | number

控制流分析

  1. 通过分析代码流程进行缩小类型
1
2
3
4
5
6
7
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
// padding是数字的情况下,下面这行代码不会执行 所以这里只能是类型 string 这就是 控制流分析
return padding + input;
}

使用类型谓词

  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
    type 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}

function getArea(shape: Shape) {
if (shape.kind === "circle") {
// 依然不可以直接使用 radius 因为它是可选的
// 但实际上我们知道 kind 是 circle 时,一定有 radius,这样要么使用 非空断言 要么 使用类型保护
return Math.PI * shape.radius! ** 2;
} else {
return shape.sideLength! ** 2;
}

// 但实际上是不理想的,理想的状态应该是 kind 是 `circle` 一定有 `radius` 是 `square` 一定有边长
}

那么我们需要将 Shape 分成两种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Circle = {
kind: "circle";
radius: number;
};

type Square = {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.sideLength ** 2;
}
}

// 这样就摆脱了错误
  1. 当联合类型中的每个类型都包含一个与文字类型相同的属性时,ts 认为这是一个有区别的联合类型,并且可以缩小联合类型的成员
  2. 上面的例子 kind 是公共成员,检查 kind 是 circle 就可以剔除 Shape 中所没有 circle 类型属性的类型,同样的检查也适用于 switch 语句
1
2
3
4
5
6
7
8
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}

never 类型与穷尽性检查

  1. 在缩小范围中,可以将一个联合体的选项减少到已经删掉了所有可能性并且什么都不剩的程度,在这种情况下,我们可以将其称为 never 类型 不存在的状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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. 描述一个函数的最简单的方法就是用一个函数类型表达式。这些类型在语法上类似于箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function greeter(fn: (a: string) => void) {
fn("hello world");
}

function printToConsole(s: string) {
console.log(s);
}

greeter(printToConsole);

// fn: (a: string) => void 一个string 参数的函数 没有返回值

// 当然我们也可以使用一个类型来命名一个函数类型

type GreetFunction = (a: string) => void;

function greeter(fn: GreetFunction) {
// ...
}

调用签名

  1. 在 js 中,函数除了可以调用以外,还可以拥有属性。然后,函数类型表达式的语法不允许声明属性。如果我们想用属性来描述可调用的东西,我们可以在一个对象类型中写入一个调用签名
  2. 注意,和函数类型表达式相比,语法略有不同:在参数列表和返回类型之间使用 : 而不是 =>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type DescriptionFunction = {
description: string;
(num: number): boolean;
}


function isOdd(fn: DescriptionFunction) {
return fn(1);
}

function fn1(num: number) {
return num % 2 === 1;
}

fn.description = "1111";

isOdd(fn);

构造签名

  1. js 函数也可以使用 new 操作符来调用,typescript 称为构造函数,它们通常会创建一个新的对象,你可以通过在调用签名前面添加 new 关键字来写一个构造签名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class 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);
  2. 还有些对象,如 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
    49
    interface 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. 在写一个函数时,输入的类型与输出的类型有关,或者两个输入的类型以某中方式相关,那么就可以使用泛型函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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"]));

类型推断

  1. 在上面的例子中我们没有指定类型,类型是由 ts 自动推断出来的,当然也可以使用多个类型参数
1
2
3
4
5
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}

const parsed = map(["1", "2", "3"], n => parseInt(n));

限制条件

  1. 我们可以对任何类型的值进行操作,有时我们想把两个值联系起来,但只能对某个值的子集进行操作,在这种情况下,我们可以使用一个约束条件来限制一个类型参数可以接受的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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
  2. 在上面的例子中,有一些有趣的事情需要注意,我们允许 Ts 推断 longest 的返回类型,返回类型推断也适用于通用函数,我们将 T 约束为 { length: number },所以我们才可以访问 a b 两个参数的 .length 属性,如果没有类型约束,我们不可以访问这些属性,因为这些值可能是一些没有长度属性的其他类型

  3. Array string 的类型是根据参数推断出来的,所以泛型就是把两个或多个具有相同类型的值联系起来

  4. 最后,正如我们希望的,longest(10, 20) 会报错,因为数字类型没有 .length 属性

使用受限值

  1. 这里有一个适用通用约束条件的常见错误

    1
    2
    3
    4
    5
    6
    7
    function minimumLength<T extends { length: number }>(obj: T, minimum: number): T {
    if (obj.length >= minimum) {
    return obj;
    } else {
    return { length: minimum };
    }
    }
  2. 看起来上面的函数是没有问题的,T 被限制为 { length: number },而且这个函数要么返回 T 要么返回一个与该限制相匹配的值,问题是,该函数承诺返回与传入参数相同的类型,而不仅仅是和约束条件想匹配的对象,如果这段代码是合法的,那么你可以得到这样一个无法工作的代码

    1
    2
    const arr = minimumLength([1, 2, 3], 4);
    console.log(arr.slice(1));

指定类型参数

  1. TS 通常可以推断出通用调用中的预期类型参数,但并非总是如此

    1
    2
    3
    4
    5
    6
    7
    8
    function 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. 类型参数下推

    • 规则: 在可能的情况下,使用类型参数本身,而不是对其进行约束
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function 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
  2. 使用更少的类型参数

    • 规则: 总是尽可能少的使用类型参数
    1
    2
    3
    4
    5
    6
    7
    8
    function 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);
    }
  3. 类型参数应出现两次

    • 规则: 如果一个类型的参数只出现在一个地方,请重新考虑是否真的需要
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function greet<Str extends string>(s: Str) {
    console.log("hello, " + s);
    }

    greet("world");

    // 为什么不直接使用 string 类型呢
    function greet(s: string) {
    console.log("hello, " + s);
    }

可选参数

  1. js 中的函数经常需要一个可变数量的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f(n: number) {
    console.log(n.toFixed()); // 0 个参数
    console.log(n.toFixed(1)); // 1 个参数
    }

    function fn1(x?: number) {
    // ...
    }

    fn1(); // 正确
    fn1(10); // 正确
  2. 上面 fn1 函数参数 x 虽然指定为 number 但因为它是可选参数,所以它实际上具有 number | undefined 类型

  3. 也可以提供默认参数值,现在 fn1 的主体中,x 将具有 number 类型,因为任何 undefined 类型都会被替换为 10

  4. 请注意,当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个丢失的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fn1(x = 10) {
    // ...
    }

    declare function f(x?: number): void;

    f();
    f(10);
    f(undefined);

回调中的可选参数

  1. 一旦了解可选参数和函数类型表达式,在编写调用回调的函数时就很容易犯错

  2. 规则: 当为回调写一个函数类型时,永远不要写一个可选参数,除非你打算在不传递该参数的情况下它还可以工作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function 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. 当一些函数可以在不同的参数数量和类型中被调用,可以通过编写重载签名来指定一个可以不同方式调用的函数,要做到这一点,要写一些数量的函数签名(通常是两个或多个),然后是函数的主体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function 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
  2. 这里我们写了两个重载: 一个接受一个参数,另一个接受三个参数。前两个签名被称为重载签名,然后我们写了一个具有兼容签名的函数实现。函数有一个实现签名,但是这个签名不能被直接调用。即使我们写了一个所需参数之后有两个可选的参数,但是也不能以两个参数调用

重载签名和实现签名

  1. 这是一个常见的混乱来源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fn(x: string): void;
    function fn() {
    // ...
    }

    // 期望以零参数调用
    // 但是错误,提示未提供 x 变量
    fn(); // error
    fn(10); // 所以只能这样调用,那么这样使用重载签名的意义何在
  2. 用于编写函数体的签名不能从外面看到

    • 实现的签名从外面是看不到的,在编写重载函数时,应该总是在函数的实现上有两个或两个以上的重载签名
  3. 实现签名也必须与重载签名兼容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function 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. 和泛型一样,在使用函数重载时,有一些准则需要遵循

    • 在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function 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 的声明

  1. 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
    38
    const 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

需要了解的其他类型

  1. 一些函数的上下文中特别相关的类型

void

  1. void 表示没有返回值的函数的返回值。当一个函数没有任何返回语句,或者没有从这些返回语句中返回任何明确的值时,它都是推断出来的类型

  2. 在 js 中,一个不返回任何值的函数将隐含的返回 undefined 的值,然而在 ts 中,void 和 undefined 是不一样的

  3. 记住 void 和 undefined 不一样

    1
    2
    3
    4
    // 推断返回的类型是 void
    function noop() {
    return;
    }

object

  1. 特殊类型 object 指的是任何不是基元的值 string number bigint boolean symbol null undefined 之外的。这和空对象类型 {} 不同,也与全局类型 Object 不同。你可能永远不会使用到 Object
  2. object 不是 Object 始终使用 object
  3. 注意:在 js 中,函数是对象,它们有属性,在它们的原型链上有 Object.prototype,是 object 的实例,可以对它们调用 Object.keys等。由于这些原因,函数类型在 Typescript 中被认为是 object

unknown

  1. unknown 类型代表任何值,这和 any 类型类似,但是更安全,因为对未知 unknown 值做任何事情都是不合法的
  2. 这对于描述函数类型非常有用,因为你可以描述接受任何值的函数,而不需要在函数体有 any 值。反之,你可以描述一个返回未知类型的值的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
function f1(a: any) {
a.b(); // 正确
}

function f2(a: unknown) {
a.b(); // error
}

function safeParse(s: string): unknown {
return JSON.parse(s);
}

const obj = safeParse('{"a": 1}'); // 要小心对待 obj

never

  1. 有些函数永远不会返回一个值
  2. never 类型表示永远不会被观察到的值,在一个返回类型中,这意味着函数抛出一个异常或终止程序的执行
  3. never 也出现在 TypeScript 确定一个 union 中没有任何东西的时候
1
2
3
4
5
6
7
8
9
10
11
12
13
function fail(msg: string): never {
throw new Error(msg);
}

function fn(x: string | number) {
if (typeof x === "string") {
//
} else if (typeof x === "number") {
//
} else {
x; // never 类型
}
}

Function

全局性的 Function 类型描述了诸如 bind call apply 和其他存在于 JS 中所有函数值的属性。它还有一个特殊的属性。即 Function 类型的值总是可以被调用,这些调用返回 any

1
2
3
4
5
// 这是一个无类型的函数调用,一般来说最好避免,因为 any 返回类型都不安全
// 如果你需要接受一个任意的函数,但不打算调用它,一般来说 () => void 是安全的
function doSomething(f: Function) {
return f(1, 2, 3);
}

参数展开运算符

形参展开

  1. 除了使用可选参数或重载来制作可以接受各种固定参数数量的函数外,还可以使用休止参数来定义接受无限制数量参数的函数
  2. rest 参数出现在所有其他参数之后,并使用 ... 语法
  3. 在 ts 中,这些参数的类型注解是隐含的 any[] 而不是 any,任何给出的类型注解必须是 Array<T> 或 T[] 的形式,或一个元组类型
1
2
3
4
5
function mulitiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}

const a = mulitiply(10, 1, 2, 3, 4);

实参展开

  1. 反之,我们可以使用 spread 语法从数组中提供可变数量的参数,例如,数组的 push 方法需要任意数量的参数

    1
    2
    3
    4
    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];

    arr1.push(...arr2);
  2. 请注意,一般来说 TS 并不假定数组是不可变的,这可能会导致一些意想不到的行为

1
2
const args = [8, 5];
const angle = Math.atan2(...args); // error 它不认为 args的数量是两个即使使用了 const 进行定义 除非使用 as const

参数解构

  1. 可以使用参数重构来方便的将作为参数提供的对象,解压到函数主体的一个或多个局部变量中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function 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 类型

  1. 函数的 void 返回类型可以产生一些不同寻常的行为,但却是预期的行为
  2. 返回类型 void 的上下文类型并不强迫函数不返回东西。另一种说法是,一个具有 void 返回类型的上下文函数类型 ( type vf = () => void),在实现时,可以返回任何其他的值,但它会被忽略,因此,以下 () => void 类型的实现是有效的
  3. 需要注意,当一个字面的函数定义返回类型是 void,该函数不能返回任何东西
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type voidFunc = () => void;

const f1: voidFunc = () => {
return 1;
}

const f2: voidFunc = () => {
return true;
}

const f3: voidFunc = function() {
return "hello";
}

// 当这些函数之一的返回值被分配给另一个变量时,它将保留 void 类型
const v1 = f1();
const v2 = f2();
const v3 = f3();

对象类型

  1. 在 js 中,我们分组和传递数据的基本方式是通过对象,我们通过对象类型来表示这些对象

  2. 对象类型可以是匿名的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function 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. 对象类型中的每个属性都可以指定几件事: 类型、属性是否可选,以及属性是否可以被写入

可选属性

  1. 在属性后使用 ? 表示它是可选的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Shape = {};

interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}

function paintShape(opts: PaintOptions) {
// ...
}

const shape: Shape = {};
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

在 js 中,即使该属性从未被设置过,我们任然可以访问它-它只是为定义的值

1
2
3
4
5
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos || 0;
let yPos = opts.yPos || 0;
// ...
}

注意,这种未指定的值设置默认值非常普遍,以至于 js 有特殊的语法来支持它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
//...
}

/**
* 为 paintShape 的参数使用了解构,并未 x y提供了默认值,现在x,y肯定都
* 存在于 paintShape 的主体中,但对于 `paintShape` 来说的任何调用者来说是可选的
*
* 注意:目前还没有办法将类型注释放在结构模式中,这是因为下面的语法在 js 中有了不同的含义
*/

function draw({ shape: Shape, xPos: number = 100 }) {

}

// 这里是将参数对象中的 shape 换成临时变量 Shape xPos 换成 number 标识符

只读属性

  1. 对于 TS 属性可以被标记为只读,虽然它不会在运行时改变任何行为,但是在类型检查期间,一个标记为只读的属性不能被写入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface SomeType {
    readonly prop: string;
    }

    function doSome(obj: SomeType) {
    console.log(obj.prop);

    obj.prop = "hello"; // error
    }
  2. 使用 readonly 修饰符并不一定意味着一个值是完全不可改变的,或者换句话说,它的内部内容不能被改变,它只意味着该属性本身不能被重新写入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    interface 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
    }
  3. 管理对 readonly 含义的预期很重要,在 ts 开发中,对于一个对象应该如何被使用的问题,它是有用的信号。ts 在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是否是 readonly 所以 readonly 属性也可以通过别名来改变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    interface 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. 有时并不提前知道一个类型的所有属性名称,但知道值的形状,在这种情况下,可以使用索引签名来描述可能的值的类型

    1
    2
    3
    4
    5
    6
    // [index: number] 索引属性,表示当一个 StringArray 被数字索引时,返回一个字符串
    interface StringArray {
    [index: number]: string;
    }

    let myArray: StringArray = ["1", "2"];
  2. 索引签名的属性类型必须是 string 或 number

  3. 支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型。这是因为当用 number 进行索引时,js 实际上会在索引到一个对象之前将其转换为 string。这意味着用 100 进行索引和用 '100' 进行索引是一样的,所以两者需要一致

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface 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;
    }
  4. 虽然字符串索引签名是描述字典模式的一种强大方式,但它也强制要求所有的属性与它们的返回类型想匹配。这是因为字符串索引声明 obj.proerty 也可以作为 obj["property"] 在下面的例子中,name 的类型与字符串索引的类型不匹配

    1
    2
    3
    4
    5
    interface NumberDictionary {
    [index: string]: number;
    length: number; // ok
    name: string; // error 类型 string 的属性不能赋值给 string 索引类型 number
    }
  5. 然而,如果索引类型是属性类型的联合,不同类型的属性是可以接受的

    1
    2
    3
    4
    5
    interface NumberOrStringDictionary {
    [index: string]: number | string;
    length: number; // ok
    name: string; // ok
    }
  6. 最后,索引签名也可以为只读属性,以防止对其索引的赋值

    1
    2
    3
    4
    5
    6
    interface ReadonlyStringArray {
    readonly [index: number]: string;
    }

    let array: ReadonlyStringArray = ["1", "2"];
    array[2] = "3"; // error readonly

扩展类型

  1. 有些类型可能是其他类型的更具体的版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface BasicAddress {
    name?: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
    }

    // 现在需要一个地址的单元
    interface AddressWithUnit extends BasicAddress {
    unit: string;
    }
  2. 接口的 extends 关键字,允许我们有效地从其他命名的类型中复制成员,并添加我们想要的任何新成员,这对于减少重复非常有用,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的

  3. 接口也可以从多个类型中扩展

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    interface Colorful {
    color: string;
    }

    interface Circle {
    radius: number;
    }

    interface ColorfulCircle extends Colorful, Circle {}

    const cc: ColorfulCircle = {
    color: "red",
    radius: 200
    }

交叉类型

  1. 接口允许通过扩展其他类型建立新的类型。而交叉类型可以通过组合其他类型建立新的类型

  2. 交叉类型用 & 操作符定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    interface 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);

接口与交叉类型

  1. 接口扩展和交叉类型非常相似,但实际上有些细小的不同,对于接口,我们可以使用 extends 子句来扩展其他类型,而对于交叉类型,我们也可以做类似的事,并用类型别名来命名新类型。
  2. 两者之间的主要区别在于如何处理冲突,这种区别通常是在接口和交叉类型的类型别名之间选择的一个主要原因
  • 接口 VS. 交叉类型
    • 相同点
      • 都可以描述对象或函数
      • 都可以扩展其他类型
    • 区别
      • 不同的声明范围
        • 接口: 声明中,值是具体结构对象
        • 交叉: 可以为任意的类型创建类型别名
      • 不同的扩展形式
        • 接口: extends
        • 交叉: &
      • 不同的重复定义表现形式
        • 接口: 自动合并
        • 交叉: 报错
    • 如何选择
      • 建议优先选择接口
      • 接口满足不了再使用交叉类型
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
// 接口可以定义多次,多次的声明会自动合并

interface Sister {
name: string;
}

interface Sister {
age: number;
}

const si: Sister = { // error 缺少 age 属性
name: "suxi"
}

const sis: Sister = {
name: "suxi",
age: 12
}

// 类型别名定义多次会报错
type Sister = {
name: string;
}

type Sister = { // error 不可以重复定义类型
age: number;
}

范型对象类型

  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;
    }
  2. 值得注意的是,类型别名也可以通用的,我们可以通过使用类型别名来代替

    1
    2
    3
    type Box<T> = {
    contents: T;
    }
  3. 由于类型别名与接口不同,它不仅可以描述对象类型,还可以用它来编写其他类型的通用辅助类型

    1
    2
    3
    4
    type OrNull<T> = T | null;
    type OneOrMany<T> = Type | Type[];
    type OneOrManyOrNull<T> = orNull<OneOrMany<T>>;
    type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
  4. 通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种形式工作是理想的,这样它们就可以在不同的数据类型中重复使用

数组类型

  1. 我们一直在使用这样一种类型: 数组类型,number[] string[] 这种类型 实际上是 Array<number> 和 Array<string> 的缩写

    1
    2
    3
    4
    5
    6
    7
    function doSome(value: Array<string>) {
    // ...
    }

    let myArray: string[] = ["hello", "hell"];

    doSome(myArray);
  2. 和上面的 Box 类型一样,Array 本身也是一个通用类型

    1
    2
    3
    4
    5
    6
    interface Array<T> {
    length: number;
    pop(): T | undefined;
    push(...items: T[]): number;
    // ...
    }
  3. 现代 JS 还提供了其他通用的数据结构,比如 Map<K, V> Set<T> Promise<T> 。这意味着,由于 Map Set Promise 的行为方式,它们可以与任何类型的集合一起工作

只读数组类型

  1. ReadonlyArray 是一个特殊的类型,描述了不应该被改变的数组

    1
    2
    3
    4
    5
    6
    function doStuff(values: ReadonlyArray<string>) {
    const copy = values.slice();
    console.log(values[0]);

    values.push("hello"); // error
    }
  2. 和属性的 readonly 修饰符一样,它主要是我们用来了解意图的工具。当我们看到一个返回 ReadonlyArray 的函数时,我们知道我们不能改变它的内容。当我们看一个接受 ReadonlyArray 的函数的时候,我们可以将任何数组传入而不用担心它会改变其内容

  3. 和 Array 不同,没有一个我们可以使用的 ReadonlyArray 构造函数

    1
    new ReadonlyArray("red"); // error
  4. 相反,我们可以将普通的 Array 分配给 ReadonlyArray

    1
    const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
  5. 正如 TypeScript 为 Array<T> 提供了 T[] 的速记语法一样,它也为 ReadonlyArray<T> 提供了只读 Type[] 的速记语法一样

    1
    2
    3
    4
    function doStuff(values: readonly string[]) {
    const copy = values.slice();
    values.push("hllo"); // error
    }
  6. 最后需要注意的是,与 readonly 属性修改器不同,可分配性在普通 Array 和 ReadonlyArray 之间不是双向的

    1
    2
    3
    4
    5
    let x: readonly string[] = ["red", "green", "blue"];
    let y: strng[] = [];

    x = y; // 非readonly 可以分配给 readonly
    y = x; // error readonly 不可以分配给非readonly

元组类型

  1. 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];
    }
  2. 可以使用数组析构来对元组进行解构

    1
    2
    3
    4
    5
    function doSuff(pair: [string, number]) {
    let [str, num] = pair;
    console.log(str);
    console.log(num);
    }
  3. 除了长度检查,像这样的简单元组类型等同于 Array 的版本,它为特定的索引声明属性,并且用数字字面类型声明长度

    1
    2
    3
    4
    5
    6
    7
    interface StringNumberPair {
    length: 2;
    0: string;
    1: number;
    // 其他 Array<string | number> 成员
    slice(start?: number, end?: number): Array<string | number>
    }
  4. 元组可以通过在元素的类型后面写出问号 ? ---- 可选的元组,元素只能出现在末尾,而且还影响到长度的类型

    1
    2
    3
    4
    5
    6
    type Either2dOr3d = [number, number, number?];

    function setCoordinate(coord: Either2dOr3d) {
    const [x, y, z] = coord;
    console.log(x, y, z);
    }
  5. 元组也可以有其余元素,这些元素必须是 array/tuple 类型

    • StringNumberBooleans 描述了一个元组,其前两个元素分别是字符串和数字,但后面可以有任意数量的布尔
    • StringBooleansNumber 描述了一个元组,其第一个元素是字符串,最后一个元素是数字,中间可以是任意数量的布尔
    • BooleansStringNumber 描述了一个元组,其倒数第二个元素是字符串,最后一个元素是数字,起始位置可以是任意数量的布尔
    1
    2
    3
    type StringNumberBooleans = [string, number, ...boolean[]];
    type StringBooleansNumber = [string, ...boolean[], number];
    type BooleansStringNumber = [...boolean[], string, number];
  6. ts 允许将 tuples 与参数列表想对应。当想用一个其余参数接受可变数量的参数,并且需要一个最小的元素数量,但不想引人中间变量时,这很方便

    1
    2
    3
    4
    5
    6
    7
    8
    function readButtonInput(...args: [string, number, ...boolean[]]) {
    // ...
    }

    // 基本等同于
    function readButtonInput(name: string, version: number, ...input: boolean[]) {
    //...
    }

只读元组类型

  1. tuple 类型有只读特性,可以通过在它们前面添加一个 readonly 关键字修饰符来指定

    1
    2
    3
    function doS(pair: readonly [string, number]) {
    pair[0] = "hello"; // error 不可写入
    }
  2. 在大多数代码中,元组往往被创建并不被修改,所以在可能的情况下,将类型注释为一个字读元组是一个很好的默认。这一点很重要,因为带有 const 的断言的数组字面量将被推断为只读元组类型

    1
    2
    3
    4
    5
    6
    7
    let point = [3, 4] as const;

    function distance([x, y]: [number, number]) {
    return Math.sqrt(x * x + y * y); // error 因为不能保证元组不被修改,所以错误
    }

    distance(point);

类型操纵

  1. 从类型中创建类型,ts 允许用其他类型的术语来表达类型
  2. 实际上有各种各样的类型操作符可以使用。也可以用我们已经有的值来表达类型
  3. 通过结合各种类型操作符,我们可以用一种简洁,可维护的方式来表达复杂的操作和值
    • 泛型型 - 带参数的类型
    • Keyof 类型操作符 - keyof 操作符创建新类型
    • Typeof 类型操作符 - 使用 typeof 操作符来创建新的类型
    • 索引访问类型 - 使用 Type["a"] 语法来访问一个类型的子集
    • 条件类型 - 在类型系统中像 if 语句一样行事的类型
    • 映射类型 - 通过映射现有类型中的每个属性来创建类型
    • 模版字面面类型 - 通过模版字面字符串改变属性的映射类型

泛型

  1. 在函数泛型已经介绍了泛型可以指定类型参数或者由 TS 进行推断

通用类型变量

  1. 当我们使用泛型时,编译器会强制要求在函数主体中正确使用泛型参数,也就是说,实际上把这些参数当作是任何和所有的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function 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. 泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似

    1
    2
    3
    4
    5
    function identity<T>(arg: T): T {
    return arg;
    }

    let myIdentity: <U>(arg: U) => U = identity;

泛型类

一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个 <> 中的泛型参数列表

1
2
3
4
5
6
7
8
9
10
11
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();

myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
return x + y;
}

和接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作

一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例而非静态侧具有通用性,所有在使用类时,静态成员不能使用类的类型参数

泛型约束

  1. 我们希望限制这个函数 any 和所有类型一起工作,而不是与 any 和所有同时具有 .length 属性的类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。必须把我们的要求作为一个约束条件列在 Type 可以是什么
1
2
3
4
5
6
7
8
9
10
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

loggingIdentity({ length: 10, value: 3 });

在泛型约束中使用类型参数

可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m"); // error

在泛型中使用类类型

在 TS 中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型

1
2
3
function create<T>(c: { new (): T }): T {
return new c();
}

一个更高级的例子,使用原型属性来推断和约束类型的构造函数和实例之间的关系

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
class Beekeeper {
hasMask: boolean = true;
}

class ZooKeeper {
nametag: string = "Mikle";
}

class Animal {
numLegs: number = 4;
}

class Bee extends Animal {
keeper: Beekeeper = new Beekeeper();
}

class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}

function createInstance<A extends Animal>(c : { new (): A } ): A {
return new c();
}

createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

keyof 类型操作符

keyof 运算符接收一个对象类型,其产生其键的字符串或数字字面联合

1
2
3
4
5
6
type Point = { x: number; y: number; };
type P = keyof Point;

const p1: P = "x";
const p2: P = "y";
const p3: P = "z"; // error

如果该类型有一个字符串或数字索引签名,keyof 将返回这些类型

1
2
3
4
5
6
7
8
9
10
11
12
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;

const a: A = 0;
const a1: A = "0"; // error

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;

const m: M = "a";
const m2: M = 10;
const m3: M = true; // error

注意上面这个例子中,M 是 string | number – 这是因为 JavaScript 对象的键总是被强制为字符串,所以 obj[0] 总是和 obj['0'] 相同

keyof 类型在与映射类型结合时变得特别有用

Typeof 类型操作符

1
2
3
4
5
let s = "hello";
let n: typeof s;

n = "world";
n = 100; // error 不能将 number 分配给 string

typeof 对基本类型来说不是很有用,但结合其他类型操作符,可以使用 typeof 方便的表达许多模式。

这里介绍一个预定义的类型 ReturnType<T> 它接受一个函数类型并产生其返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
type Predicate = (x: unknown) => boolean;

type K = ReturnType<Predicate>;

// 当我们试图在一个函数名上使用 ReturnType 会发生错误
function f() {
return { x: 10, y: 3 };
}

// 请记住 值和类型不是一回事,为了指代值 f 的类型,我们使用 typeof
type P = ReturnType<f>; // error

type P = ReturnType<typeof f>; // { x: number; y: number;}

ts 故意限制了可以使用 typeof 表达式的种类,具体来说,只有在标识符或其属性上使用 typeof 是合法的,这有助于避免混乱的陷阱

1
let shouldContinue: typeof msgbox("helelo"); // error

索引访问类型

我们可以使用索引访问类型来查询另一个类型上的特定属性

1
2
3
4
5
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number

const ag1: Age = 10;
const ag2: Age = "11"; // error

索引类型本身就是一个类型,所以我们可以完全使用 unions keyof 或其他类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string;
age: number;
alive: boolean;
}

type I1 = Person["age" | "name"]; // string | number

const i1: I1 = "11";
const i2: I1 = 11;

type I2 = Person[keyof Person]; // string | number | boolean

const i3: I2 = "";
const i4: I2 = 11;
const i5: I2 = false;

如果试图索引一个不存在的属性

1
type I1 = Person["alve"]; // error

另一种使用任意类型进行索引的例子是使用 number 来获取一个数组元素的类型,我们可以把它和 typeof 结合起来,方便地获取一个数组字面的元素类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 }
];

type Person = typeof MyArray[number];

const p1: Person = {
name: "suxi",
age: 18
}

type Age = typeof MyArray[number]["age"];
const age1: Age = 10;

type Age2 = Person["age"];

const age2: Age2 = 10;

只能在索引时使用类型,这意味着不能使用 const 来做一个变量引用

1
2
3
4
5
6
const key = "age";
type Age = Person[key]; // error // key 在这里用作值而不是类型


type Key = "age";
type Age = Person[key];

条件类型

  1. 在大多数有用的程序的核心,我们必须根据输入来做决定 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
    32
    interface 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";
    }
  2. createLabel 的这些重载描述了一个单一的 js 函数,该函数根据其输入的类型作出选择。注意一些事情:

    • 如果一个库必须在其 API 中反复作出同样的选择,这就会变得很麻烦
    • 必须创建三个重载:一个用于确定类型的情况,一个用于最一般的情况,对于 createLabel 所能处理的每一种新类型,重载的数量都会呈指数级增长
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type 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. 条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Message<T> = T["message"]; // error 类型 message 无法用于索引类型 T

    type MessageOf<T extends { message: unknown }> = T["message"];

    interface Email {
    message: string;
    }

    type EmailMessageContents = MessageOf<Email>;
  2. 然而,如果我们想要让 MessageOf 接受任何类型,并在消息属性不可用的情况下,默认为 never 类型呢,我们可以通过约束条件移出,并引入一个条件类型来做到这一点

    1
    2
    3
    4
    5
    6
    7
    type MessageOf<T> = T extends { message: unknown } ? T[message] : never;
    type Dog = {
    bark(): void;
    }
    type DogMessageContents = MessageOf<Dog>;

    const dmc: DogMessageContents = "error" as never;
  3. 在真正的分支中,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;

在条件类型内进行推理

  1. 我们只是发现自己使用条件类型来应用约束条件,然后提取出类型,这最终成为一种常见的操作,而条件类型使得它变得更容易

  2. 条件类型为我们提供了一种方法来推断在真实分支中使用 infer 关键字进行对比的类型。例如,我们可以在 Flatten 中推断出元素类型,而不是用索引访问类型手动提取出来

    1
    type Flatten<T> = T extends Array<infer Item> ? Item : T;
  3. 上面的例子,我们使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如何在真实分支中检索 T 的元素类型,这使得我们不必考虑如何挖掘和探测我们感兴趣的类型的结构

  4. 我们可以使用 infer 关键字编写一些有用的辅助类型别名,例如,对于简单的情况,我们可以从函数类型中提取出返回类型

    1
    2
    3
    4
    5
    6
    7
    type 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>;
  5. 当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推测,这是最容许的万能情况),不可能根据参数类型的列表来执行重载解析

    1
    2
    3
    4
    5
    declare 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. 当条件类型作用于一个通用类型时,当给定一个联合类型时,它们就变成了分布式的。例如,以下面的例子为例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    type 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. 映射类型建立在索引签名的语法上,索引签名用于声明没有被提前声明的属性类型

    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
    type 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>;

映射修改器

  1. 在映射的过程中,有两个额外的修饰可以应用: readonly 和 ? 它们分别影响可变性和可选性

  2. 可以通过 - 或 + 作为前缀来删除或添加这些修饰语。如果不加前缀,那么假定是 +

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    type 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 重映射

  1. 可以通过映射类型中的 as 子句重新映射映射类型中的键

    1
    2
    3
    4
    // 语法
    type MappedTypeWithNewProperties<T> = {
    [Property in keyof T as NewKeyType]: T[Property];
    }
  2. 利用模版字面类型等功能,从先前的属性名称中创建一个新的属性名称

    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"
    }
  3. 过滤掉某些属性

    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>;
  4. 可以映射任意的联合体,不仅仅是 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>;
  5. 映射类型与本类型操作部分的其他功能配合得很好,例如,这里有一个使用条件类型的映射类型,它根据一个对象的属性 pii 是否被设置为字面意义上的 true 返回 true 或 false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type 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. 在一个类上声明字段,创建一个公共的可写属性

    1
    2
    3
    4
    5
    6
    7
    8
    class Point {
    x: number;
    y: number;
    }

    const pt = new Point();
    pt.x = 0;
    pt.y = 0;
  2. 和其他位置一样,类型注解是可选的,但如果不指定,将是一个隐含的 any 类型

  3. 字段也可以有初始化器,这些初始化器将在类被实例化时自动运行

    1
    2
    3
    4
    5
    6
    7
    class Point {
    x = 0;
    y = 0;
    }

    const pt = new Point();
    console.log(`${pt.x}, ${pt.y}`)
  4. 和 const let var 一样,一个类属性的初始化器将被用来推断其类型

    1
    2
    const pt = new Point();
    pt.x = "0"; // error
  5. tsconfig.json 中 strictPropertyInitialization 设置控制是否需要在构造函数中初始化实例字段

    1
    2
    3
    4
    5
    6
    7
    // 设置了 strictPropertyInitialization 为 true 需要在 构造函数中 初始化 字段
    class GoodGreeter {
    name: string;
    constructor() {
    this.name = "hello";
    }
    }
  6. 但是注意,字段需要在构造函数本身中初始化。TypeScript 不会分析从构造函数中调用的方法来检测初始化,因为派生类可能会覆盖这些方法而无法初始化成员

  7. 如果打算通过构造函数以外的方式来确定初始化一个字段(例如,也许一个外部库为你填充了你的类的一部分),可以使用确定的赋值断言操作符 !

    1
    2
    3
    4
    // 没有初始化,但没有报错
    class OKGreeter {
    name!: string;
    }

readonly

  1. 字段的前缀可以是 readonly 修饰符。这可以防止在构造函数之外对该字段进行赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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. 类构造函数与函数非常相似。可以添加带有类型注释的参数、默认值和重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class 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) {
    // ...
    }
    }
  2. 类的构造函数签名和函数签名之间只有一些区别

    • 构造函数不能有类型参数 - 这属于外层类的声明
    • 构造函数不能有返回类型注释 - 类的实例总是被返回
  3. super 调用

    • 和 js 一样,如果你有一个基类,在使用任何 this. 成员之前,需要在构造器主体中调用 super
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base {
    k = 4;
    }

    class Derived extends Base {
    constructor() {
    // 在 ES5 中打印一个错误的值,在 ES6 中抛出异常
    // 必须在访问派生类的 this 前调用 super
    console.log(this.k);
    super();
    }
    }

方法

  1. 一个类上的函数属性被称为方法,方法可以使用函数和构造函数相同的所有类型注释

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point {
    x = 10;
    y = 10;

    scale(n: number): void {
    this.x *= n;
    this.y *= n;
    }
    }
  2. 除了标准的类型注解,ts 并没有为方法添加其他新的东西

  3. 注意,在一个方法体中,仍然必须通过 this 访问字段和其他方法,方法体中的非限定名称将总是指代包围范围内的东西

    1
    2
    3
    4
    5
    6
    7
    8
    let x: number = 0;

    class C {
    x: string = "hello";
    m() {
    x = "world"; // error 修改的是 第一行的 x
    }
    }

Getters / Setters

  1. 类也可以有访问器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class C {
    _length = 0;

    get length() {
    return this._length;
    }

    set length(value) {
    this._length = value;
    }
    }
  2. 注意,一个没有额外逻辑的字段支持的 get/set 对在 js 中很少有用,如果不需要在 get/set 操作中添加额外的逻辑,暴露公共字段也是可以的

  3. TypeScript 对访问器有一些特殊的推理规则

    • 如果存在 get 但没有 set,则该属性自动是只读的
    • 如果没有指定 setter 参数的类型,它将从 getter 的返回类型中推断出来
    • 访问器和设置器必须有相同的成员可见性
  4. 从 TypeScript4.3 开始,可以有不同类型的访问器用于获取和设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class 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
2
3
4
5
6
7
class MyClass {
[s: string]: boolean | ((s: string) => boolean);

check(s: string) {
return this[s] as boolean;
}
}

因为索引签名类型需要同时捕获方法和属性,所以要有用地使用这些类型并不容易。一般来说,最好将索引数据存储在另一个地方,而不是类实例本身

类继承

implements 子句

  1. 可以使用一个 implements 子句来检查一个类,是否满足一个特定的接口,如果一个类不能正确地实现它,就会发出一个错误

  2. 类可以实现多个接口,例如 class C implements A, B {}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    interface Pingable {
    ping(): void;
    }

    class Sonar implements Pingable {
    ping() {
    console.log("ping");
    }
    }

    class Ball implements Pingable {
    // 缺少 ping 方法
    pong() {
    console.log("pong!");
    }
    }
  3. 注意,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";
    }
    }
  4. 同样的,实现一个带有可选属性的接口并不能创建该属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface A {
    x: number;
    y?: number;
    }

    class C implements A {
    x = 0;
    }

    const c = new C();
    c.y = 10; // 类型C上不存在属性 y

extends 子句

  1. 类可以从基类中扩展出来,派生类拥有其基类的所有属性和方法,也可以定义额外的成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class 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);

重写方法

  1. 派生类也可以覆盖基类的一个字段或属性,可以使用 super. 语法来访问基类方法。注意,因为 JavaScript 类是一个简单的查找对象,没有超级字段的概念

  2. TypeScript 强制要求派生类总是其基类的一个子类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class 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");
  3. 派生类遵循其基类契约是很重要的,通过基类引用派生类实例是非常常见的(而且总是合法的)

    1
    2
    const b: Base = d;
    b.greet();
  4. 如果遵循基类的约定将会报错

    1
    2
    3
    4
    5
    6
    class Derived extends Base {
    greet(name: string) {
    // 必须的参数,报错
    console.log("hello," + name.toUpperCase());
    }
    }

初始化顺序

  1. 类初始化的顺序是:
    • 基类的字段被初始化
    • 基类构造函数运行
    • 派生类的字段被初始化
    • 派生类构造函数运行
  2. 这个顺序意味着基类构造函数在自己的构造函数中看到自己的属性,因为派生类的字段初始化还没有运行
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
name = "base";
constructor() {
console.log("my name is" + this.name);
}
}

class Derived extends Base {
name = "derived";
}

const d = new Derived();

继承内置类型

注意: 如果不打算继承 Array Error Map 等内置类型,或者编译目标明确设置为 es6/es2015 或以上,则不需要关注

  1. 在 es2015 中,返回对象的构造函数隐含地替代了 super(...) 的任何调用者的 this 的值,生成的构造函数代码有必要捕获 super(...) 的任何潜在返回并将其替换为 this

  2. 因此,子类化 Error Array 等可能不再像预期那样工作。这是由于 Error Array 等的构造函数使用 ECMAScript6 的 new.target 来调整原型链;然而,在 ECMAScript 5 中调用构造函数时,没有办法确保 new.target 的值。其他的下级编译器一般默认有同样的限制

  3. 对于下面的子类 有以下问题

    • 方法在构造这些子类所返回的对象上可能是未定义的,所以调用 sayHello 会导致错误
    • instanceof 将在子类的实例和它们的实例之间被打破,所以 new MsgError() instanceof MsgError 将返回 false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class 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
    10
    class MsgError extends Error {
    constructor(m: string) {
    super(m);
    Object.setPrototypeOf(this, MsgError.prototype);
    }

    sayHello() {
    cosnole.log("hello")
    }
    }

成员的可见性

public

  1. 类成员的默认可见性是公共的,一个公共成员可以在任何地方被访问
  2. public 是默认的可见性修饰符,所以永远不需要在类成员上书写它,除非为了风格/可读性的原因
1
2
3
4
5
6
7
8
class Greeter {
public greet() {
console.log("hi");
}
}

const g = new Greeter();
g.greet();

protected

  1. 受保护的 protected 成员只对它们所声明的类或子类可见

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class 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(); // 无权访问
  2. 受保护成员的暴露

    • 派生类需要遵循它们的基类契约,但可以选择公开具有更多能力的基类的子类型,这包括将受保护的成员变成公开
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Base {
    protected m = 10;
    }

    class Derived extends Base {
    m = 15;
    }

    const d = new Derived();
    console.log(d.m); // 没有问题

private

  1. private 和 protected 一样,但不允许从子类中访问该成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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); // 不能在子类访问
    }
    }
  2. 私有成员对于派生类是不可见的,所以派生类不能增加其可见性

  3. 跨实例的私有访问 TS 允许跨实例私有访问

    1
    2
    3
    4
    5
    6
    7
    class A {
    private x = 10;

    public sameAs(other: A) {
    return other.x === this.x;
    }
    }
  4. 像 TS 类型系统的其他方面一样,private 和 protected 只在类型检查中被强制执行

  5. 这意味着 JS 的运行时结构,如 in 或简单的属性查询,仍然可以访问一个私有或保护的成员

  6. private 也允许在类型检查时使用括号符号进行访问,这使得私有声明的字段更容易被单元测试之类的东西所访问,缺点是这些字段是软性私有的,不能严格执行私有特性

    1
    2
    3
    4
    5
    6
    7
    8
    class MySafe {
    private secretKey = 12345;
    }

    const s = new MySafe();

    console.log(s.secretKey); // error
    console.log(s["secretKey"]); // ok
  7. 与 TS 的 private 不同,JS 的 private 字段 # 在编译后仍然是 private 的,并且不提供前面提到的像括号符号访问那样的窗口,使其成为 private

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class 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

静态成员

  1. 类可以有静态成员,这些成员并不与类的特定实例相关联。它们可以通过类的构造函数对象本身来访问
  2. 静态成员也可以使用相同的 public protected 和 private 可见性修饰符
  3. 静态成员也会被继承
  4. 和 JS 一样,静态方法中的 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
class MyClass {
private static xx = 20;
static x = 0;
static printX() {
console.log(MyClass.x);
console.log(this.x); // this表示 MyClass 类本身
}
static printXX() {
console.log(this.xx);
}
}

console.log(MyClass.x);
MyClass.printX();

console.log(MyClass.xx);


class Base {
static getGreeting() {
return "hello world";
}
}

class Derived extends Base {
myGreeting = Derived.getGreeting();
}

特殊的静态名称

  1. 一般来说,从函数原型覆盖属性是不安全/不可能的,因为类本身就是可以用 new 调用函数,所以某些静态名称不能使用。像 name length 和 call 这样的函数属性,定义为静态成员是无效的
1
2
3
class S {
static name = "S!"; // error
}

类里的 static 区块

  1. 静态块允许写一串有自己作用域的语句,可以访问包含类中的私有字段,这意味着我们可以用写语句的所有能力来写初始化代码,不泄露变量,并能完全访问我们类的内部结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo {
static #count = 0;

get count() {
return Foo.#count;
}

static {
try {
const lastInstances = {
length: 100
};
Foo.#count += lastInstances.length;
} catch (e) {
console.error(e);
}
}
}

泛型类

类,和接口一样,可以是泛型的。当一个泛型类用 new 实例化时,其类型参数的推断方式与函数调用的方式相同

1
2
3
4
5
6
7
8
class Box<T> {
contents: T;
constructor(value: T) {
this.contents = value;
}
}

const b = new Box("hello");
  1. 类可以像接口一样使用通用约束和默认值
    • 静态成员不能引用类的类型参数,因为类型总是被完全擦除的
    • 一个泛型类的静态成员永远不能引用该类的类型参数

类运行时的 this

  1. 记住,TypeScript 并没有改变 JavaScript 的运行时行为,而 JavaScript 的运行时行为偶尔很奇怪

  2. 比如,JavaScript 对这一点的处理确实是不寻常的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class MyClass {
    name = "MyClass";

    getName() {
    return this.name;
    }
    }

    const c = new MyClass();
    const obj = {
    name: "obj",
    getName: c.getName
    }

    console.log(obj.getName()); // 输出 obj 而不是 MyClass
  3. 默认情况下,函数内 this 的值取决于函数的调用方式,在这个例子中,因为函数是通过 obj 引用调用,所以它的 this 值是 obj 而不是类实例,这很少是希望发生的事情,TS 提供了一些方法来减轻或防止这种错误

  4. 箭头函数

    • 如果有一个经常被调用的函数,失去它的 this 上下文,那么使用一个箭头函数而不是方法定义是有意义的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyClass {
    name = "MyClass";

    getName = () => {
    return this.name;
    }
    }

    const c = new MyClass();
    const g = c.getName;

    console.log(g()); // myClass
  5. 关于箭头函数,还有一些权衡

    • this 值保证在运行时是正确的,即使是没有经过 TypeScript 检查的代码也是如此
    • 这将使用更多的内存,因为每个类实例都将有它自己的副本,每个函数都是这样定义的
    • 不能在派生类中使用 super.getName,因为在原型链中没有入口可以获取基类方法
  6. this 参数出现在所有其他参数之后

    • 在方法或函数定义中,一个名为 this 的初始化参数在 TypeScript 中具有特殊的意义。这些参数在编译过程中会被删除
    1
    2
    3
    4
    5
    6
    7
    function 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
    14
    class 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 类型

  1. 在类中,一个叫做 this 的特殊类型动态地指向当前类的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class 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);
  2. 也可以在参数类型注释中使用 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Box {
    content: string = "";

    sameAs(other: this) {
    return other.content === this.content;
    }
    }

    const box = new Box();
    console.log(box.sameAs(box))
  3. 这与其他写法不同: Box 如果有一个派生类,它的 sameAs 方法现在只能接受该同一派生类或其派生类的子类的其他实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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

  1. 可以在类和接口的方法返回位置使用 this is Type。当与类型缩小混合时(例如 if 语句),目标对象的类型将被缩小到指定的 Type

    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
    class 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);
  2. 基于 this 的类型保护的一个常见用例,是允许对一个特定字段进行懒惰验证。例如,下面这种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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());
    }

参数属性

  1. TS 提供了特殊的语法,可以将构造函数变成具有相同名称和值的类属性。这些被称为参数属性,通过在构造函数参数前加上可见性修饰符 public private protected 或 readony 中的一个来创建。由此产生的字段会得到这些修饰符
1
2
3
4
5
6
7
8
9
class Params {
constructor(public readonly x: number, protected y: number, private z: number) {

}
}

const a = new Params(1, 2, 3);
console.log(a.x);
console.log(a.y);

类表达式

  1. 类表达式与类声明非常相似。唯一真正的区别是,类表达式不需要一个名字,尽管我们可以通过它们最终绑定的任何标识符来引用它们
1
2
3
4
5
6
7
8
const someClass = class<T> {
content: T;
constructor(value: T) {
this.content = value;
}
}

const m = new someClass("hello world")

抽象类和成员

  1. TypeScript 中的类、方法和字段可以是抽象的。一个抽象的方法或抽象的字段是一个没有提供实现的方法或字段。这些成员必须存在于一个抽象类中,不能直接实例化

  2. 抽象类的作用是作为子类的基类,实现所有的抽象成员。当一类没有任何抽象成员时,我们就说它是具体的

    1
    2
    3
    4
    5
    6
    7
    8
    abstract class Base {
    abstract getName(): string;
    printName() {
    console.log("Hello, " + this.getName());
    }
    }

    let b = new Base(); // error
  3. 不能用 new 关键字来实例化 Base 因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员

    1
    2
    3
    4
    5
    6
    7
    8
    class Derived extends Base {
    getName() {
    return "world";
    }
    }

    const d = new Derived();
    d.printName();
  4. 抽象构造签名

    • 如果想接受一些类的构造函数,产生一个从某些抽象类派生出来的类的实例
    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

类之间的关系

  1. 在大多数情况下,TypeScript 中的类在结构上与其他类型相同,是可以比较的

  2. 例如,这两个类可以相互替代使用,因为它们是相同的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Point1 {
    x = 0;
    y = 0;
    }

    class Point2 {
    x = 0;
    y = 0;
    }

    const p: Point1 = new Point2(); // 正确的
  3. 同样的,即使没有明确的继承,类之间的子类型关系也是存在的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Person {
    name: string;
    age: number;
    }

    class Employee {
    name: string;
    age: number;
    salary: number;
    }

    const p: Person = new Employee(); // right
  4. 这看起来很简单,但是有几种情况似乎比其他情况更奇怪。空的类没有成员,在一个结构化类型系统中,一个没有成员的类型通常是其它任何东西的超类型。所以如果你有一个空类,任何东西都可以用来代替它

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Empty {

    }

    function fn(x: Empty) {
    // 不能用 x 做任何事
    }


    fn(window);
    fn({});
    fn(fn);

模块

如何定义 JavaScript 模块

  1. 在 TypeScript 中,就像在 ECMAScript2015 中一样,任何包含顶级 import 或 export 的文件都被认为是模块
  2. 相反,一个没有任何顶级导入或导出声明的文件被视为一个脚本,其内容可在全局范围内使用(因此也可用于模块,例如 import "./index.js")
  3. 模块在自己的范围内执行,而不是在全局范围。这意味着在模块中声明的变量、函数、类等在模块外是不可见的,除非它们被明确地用某种导出形式导出。相反,要使用从不同模块导出的变量、函数、类、接口等,必须使用导入的形式将其导入

非模块

  1. JavaScript 规范声明,任何没有 export 或顶层 await 的 JavaScript 文件都应该被认为是一个脚本而不是一个模块

  2. 在一个脚本文件中,变量和类型被声明为在共享全局范围内,并且假定会使用 outFile 编译器选项将多个输入文件加入一个输出文件,或者在你的 html 中使用 script 标签来加载这些文件

  3. 如果有一个目前没有任何导入或导出的文件,但希望被当作一个模块来处理,请添加这一行:

    1
    export {}
  4. 这将改变该文件,使其成为一个什么都不输出的模块。无论你的模块目标是什么,这个语法都有效

TypeScript 中的模块

  1. 在 TypeScript 中编写基于模块的代码时,有三个主要方面
    • 语法
    • 模块解析
    • 模块输出目标

ES 模块语法

  1. 一个文件可以通过 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);

额外的导入语法

  1. 可以使用 import { old as new } 这样的格式来重命名一个导入

    1
    2
    3
    import { pi as PI } from "./math.ts";

    console.log(PI);
  2. 可以把所有导出的对象,用 * as name 放到一个命名空间

    1
    2
    3
    import * as math from "./math.ts";

    console.log(math.pi);
  3. 也可以通过 import "./file" 导入一个文件,而不把任何变量纳入你的当前模块。这种情况下,import 没有任何作用,然后 math.ts 中的所有代码都被解析了,这可能引发影响其他对象的副作用

    1
    2
    import "./math.ts";
    console.log(3.14)

TypeScript 特定的 ES 模块语法

  1. 类型可以使用与 JS 值相同的语法进行导出和导入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export type Cat = { breed: string; yearOfBirth: number; };

    export interface Dog {
    breeds: string[];
    yearOfBirth: number;
    }

    import { Cat, Dog } from "./animal.ts";
    type Animals = Cat | Dog;
  2. TS 用了两个概念扩展了 import 语法,用于声明一个类型的导入

    • import type
    1
    2
    3
    4
    5
    6
    7
    8
    export type Cat = { breed: string; yearOfBirth: number; };

    export interface Dog {
    breeds: string[];
    yearOfBirth: number;
    }

    import type { Cat, Dog } from "./animal.ts";
  3. 内联类型导入

    • TS4.5 还允许以 type 为前缀的单个导入,以表明导入的引用是一个类型
    1
    2
    3
    import { 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
2
import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

CommonJS 语法

CommonJS 是 npm 上大多数模块的交付格式

导出

标识符是通过一个全局调用的 module 上设置 exports 属性来导出的

1
2
3
4
5
6
7
8
9
10
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}

module.exports = {
pi: 3.14,
phi: 1.61,
absolute
}

这些文件可以通过 require 语句导入

1
2
3
4
const maths = require("maths");
maths.pi;

const { pi } = require("maths")

CommonJS 和 ES 模块的互操作性

  1. 关于默认导入和模块命名空间对象导入之间的区别,CommonJS 和 ES Modules 之间存在功能上的不匹配

TypeScript 的模块解析选项

  1. 模块解析是指从 import 或 require 语句中获取一个字符串,并确定该字符串所指的文件的过程
  2. TS 包括两种解析策略,经典和 Node。当编译器选项 module 不是 commonjs 时,经典策略是默认的,是为了向后兼容。Node 策略复制了 Node 在 CommonJS 模式下的工作方式,对 .ts 和 .d.ts 有额外的检查
  3. 在 TS 中,有许多 TSConfig 标志影响模块策略 moduleResolution baseUrl paths rootDirs

TS 的模块输出选项

  1. 有两个选项会影响 JS 输出

    • target 它决定了哪些 JS 功能被降级(转换在旧 js 运行时运行),哪些保持不变
    • module 它决定了哪些代码用于模块之间的相互作用
  2. 使用的 target 由你期望运行 TypeScript 代码的 JavaScript 运行时中的可用功能决定的。这可能是:支持的最古老的网络浏览器,期望运行的最低版本的 NodeJS,或者可能来自于运行时的独特约束 — electron

  3. 所有模块之间的通信都是通过模块加载器进行的,编译器选项 module 决定使用哪一个。在运行时,模块加载器负责在执行一个模块之前定位和执行该模块的所有依赖项

  4. 例如,这里是一个使用 ES 模块语法的 TypeScript 文件,展示了 module 的一些不同选项

    1
    2
    import { valueOfPi } from "./constants.js";
    export const twoPi = valueOfPi * 2;
    • ES2020
    1
    2
    import { valueOfPi } from "./constants.js";
    export const twoPi = valueOfPi * 2;
    • CommonJS
    1
    2
    3
    4
    5
    "use strict";
    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) {
    "use strict";
    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 的方向保持一致