TypeScript学习笔记

TypeScript 的好处

  • 让你的程序是确定性的,比如参数类型、返回类型

  • 提升性能,因为没有了类型判断的 if 逻辑

  • 强化数据结构的概念

  • 对重构很友好

环境搭建

普通 TS 项目可以通过这个模板创建:

https://github.com/kbysiec/vite-vanilla-ts-lib-starter

其他类型的项目,可以参考 Vite 的各种模板:

https://github.com/vitejs/awesome-vite#templates

tsconfig.json

To Be Read:

One Thing Nobody Explained To You About TypeScript

类型

基础类型

原始类型

number, string, boolean

数据结构的声明会自动提升,所以你可以先使用变量,后定义变量。

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 dataType(userName: string): string {
let age: number = 12;
let isStudent: boolean = true;
let name: string = userName;

let u: undefined = undefined;
let n: null = null;

return 'Hello, ' + name;
}

function anyType() {
let a1: any = 1;
let a2: any = 'abc';
console.log(a1.what);
}

function returnVoid(): void {
// TODO
let unusable1: void = null;
let unusable2: void = undefined;
}

function unionType() {
let myFavoriteNumber: number | string | boolean;
myFavoriteNumber = 3;
myFavoriteNumber = 'hello';
myFavoriteNumber = true;
}

数组类型

number[], Array

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
let ages: number[] = [1, 2]
ages.push(3)

// 数组泛型
let numbers: Array<number> = [1, 2, 3]

interface NumberArray{
[index: number]: number;
}

// 类数组、内置对象
function argTest() {
let args: IArguments = arguments;
}

// 二维数组
let prices: string[][] = [];
let prices: Array<Array<string>> = new Array<Array<string>>();

// 存放对象的数组
export type ChartDataSet = Array<{
name: string;
date: number;
value: string;
}>;

// 存放对象的二维数组
// 1、先定义对象结构,再定义二维数组
interface DataPoint {
x: string;
y: number;
date: string;
}
let dataPoints: DataPoint[][];

// 2、使用类型注解
let dataPoints: Array<{
x: string;
y: number;
date: string;
}[]>;

// 3、使用泛型数组
let dataPoints: Array<Array<{ x: string; y: number; date: string }>>;

元组类型

[number, string]

仅用于数组中存放不同数据类型。

因为数组的数据存储空间小一些,所以坐标会用到数组,比如:position: [1, 1]这种;其他场景,一般我们用键值对对象。

注意元组是限定了数组的长度的。

Sum Type

在编程语言和类型理论中,”Sum Type”(求和类型或和类型)是一种复合类型,它允许一个变量可以是多种类型中的一种。它通常与”Product Type”(乘积类型)相对比,后者允许一个变量同时具有多种类型的值。

Sum Type 的一个典型例子是枚举(enum)类型。在许多编程语言中,枚举允许你定义一个类型,它可以是一组预定义值中的任何一个。例如,在 C 或 C++中:

1
enum Color { RED, GREEN, BLUE };

这里的 Color 就是一个 Sum Type,因为它可以是 RED、GREEN 或 BLUE 中的任何一个值。

在函数式编程语言中,Sum Type 通常以代数数据类型(Algebraic Data Type,ADT)的形式出现,例如 Haskell 中的 Either a b 类型,它可以是类型 a 或类型 b:

1
data Either a b = Left a | Right b

这里的 Either a b 可以持有一个 Left 值,其类型为 a,或者一个 Right 值,其类型为 b。

Sum Type 在类型安全、模式匹配和错误处理等方面非常有用,因为它们允许程序员明确地表达一个值可能具有的不同形式,并且可以在编译时检查这些形式。

在 TypeScript 中,Sum Type 可以通过联合类型(Union Types)和枚举(Enums)来实现。以下是一些例子:

  1. 联合类型(Union Types)

联合类型允许你将多个类型组合成一个类型,表示一个值可以是几种类型之一。

1
2
3
4
5
6
7
8
9
10
// 联合类型示例:一个值可以是字符串或数字
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
console.log(`ID: ${id}`);
}

printId(101); // 正确,id 是数字
printId('202'); // 正确,id 是字符串
printId(true); // 错误,id 不是布尔值
  1. 枚举(Enums)

枚举是一种特殊的类型,它允许你为一组数值赋予更易读的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 枚举示例:颜色枚举
enum Color {
RED = 1,
GREEN = 2,
BLUE = 4,
}

function printColorName(color: Color) {
console.log(`Color: ${Color[color]}`);
}

printColorName(Color.RED); // 输出: Color: RED
printColorName(3); // 错误,3不是Color枚举的成员
  1. 代数数据类型(Algebraic Data Types,ADTs)

虽然 TypeScript 原生不支持代数数据类型,但你可以使用接口和类型别名来模拟它们。

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 Success<T> {
kind: 'Success';
value: T;
}

interface Error {
kind: 'Error';
message: string;
}

type Result<T> = Success<T> | Error;

function getResult(value: number): Result<number> {
if (value > 0) {
return { kind: 'Success', value };
} else {
return { kind: 'Error', message: 'Value must be positive' };
}
}

const result = getResult(-1);
if (result.kind === 'Success') {
console.log(`Result: ${result.value}`);
} else {
console.log(`Error: ${result.message}`);
}
  1. 可辨识联合(Discriminated Unions)

通过在联合类型中使用一个共有的属性来区分不同的类型,可以创建可辨识联合。

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 Square {
kind: 'square';
sideLength: number;
}

interface Circle {
kind: 'circle';
radius: number;
}

type Shape = Square | Circle;

function calculateArea(shape: Shape) {
switch (shape.kind) {
case 'square':
return shape.sideLength * shape.sideLength;
case 'circle':
return Math.PI * shape.radius * shape.radius;
}
}

const square: Square = { kind: 'square', sideLength: 4 };
const circle: Circle = { kind: 'circle', radius: 5 };

console.log(calculateArea(square)); // 输出: 16
console.log(calculateArea(circle)); // 输出: 约78.53981633974483

枚举类型

enum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自动分配值
enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}

// 明确指定值
enum FileState {
Read = 1,
Write = 2,
ReadWrite = 3,
}
为什么不推荐用枚举?

TypeScript —— 枚举类型 enum 的红与黑 - Wise.Wrong - 博客园

TypeScript = JavaScript + Types

TypeScript  设计的初衷是 JavaScript + Types,所有 TypeScript 的特性不改变运行时的行为

反过来说,如果在 TS 代码中去掉静态类型,应该得到一份完整有效的 JS 代码

这样的好处在于,我们可以通过 ESbuild 而不是 tsc 完成我们的 TS 代码到 JS 代码的转换

但实际上 TypeScript 中有一个特殊类型破坏了这种构想,它就是  Enum

比如这一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Language {
ZH_CN = 'zh_CN',
ZH_HK = 'zh_HK',
ZH_TW = 'zh_TW',
EN_US = 'en_US',
EN_GB = 'en_GB',
}

function getLocals(lang: Language) {
return `hello ${lang}`;
}

getLocals(Language.ZH_CN);

1、由于 enum 可以当做对象使用,所以如果删掉  Language,这段代码就无法运行

2、在作为静态类型使用的时候,enum 还会带来额外的心智负担,上面的  Language 如果换成联合类型的写法,可能更符合直觉:

1
type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB';

3、由于使用了 enum,我们不得不使用 tsc 而非 ESbuild 来编译项目,导致整个编译过程的开销巨大。

(精)联合类型

number | string

1
2
let a: string | number = '123';
a = 123;

(精)基于联合类型实现的高级数据结构:

https://blog.csdn.net/dajuna/article/details/117958613

比如 Record、Partial、Required、Pick、Readonly、Exclude、Omit、ReadonlyArray

联合类型是让类型具备不确定性;而基于联合类型实现的高级数据结构,则是让数据类型具备(一定的)确定性

任意类型

any

未知类型

unknown

空值类型

void, null, undefined

字面量类型

如 ‘literal’, 42

交叉类型

TypeA & TypeB

将多个类型合并为一个类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface IPerson {
id: string;
age: number;
}

interface IWorker {
companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
id: 'E1006',
age: 33,
companyId: 'EXE',
};

console.dir(staff);

TypeScript 中的 & 运算符是什么 - 咱这个需求做不了 - 博客园

对象

定义任意属性

1
2
3
4
5
6
7
8
9
interface Person {
name : string ,
[propName: string] : any;
}

let zhangsan : Person {
name : 'zhangsan',
gender : 'male'
};

注意这种情况:

1
2
3
4
5
6
7
8
9
10
interface Person {
name : string ,
age : number ,
[propName: string] : any;
}
let zhangsan : Person {
name : 'zhangsan',
age : 25 ,
gender : 'male'
};

这时候你运行代码,会发现报错了:’Property ‘age’ of type ‘number’ is not assignable to string index type ‘string’.’,这报错的意思,我们从字面理解的话就是:’类型为“number”的属性“age”不能分配给字符串索引类型“string”。’,我们再深入理解,也就是属性 age 的 number 属性不是 string 类型的子属性。这样就很奇怪了,为什么会报这个错误?

我们可以看下官方文档,原来如果我们在定义接口对象时定义了任意属性,那么这个接口对象内其余的属性就必须是任意属性的子属性,因为我们定义了一个 age 属性,它的类型是 number 类型的,而 number 类型不是 string 类型以及它的子类型,所以编译时就报错了。
————————————————
版权声明:本文为 CSDN 博主「Crimaster·W」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Kreme/article/details/102699712

缺省属性

通过问号指定:

1
2
3
4
5
6
7
8
9
10
interface Person {
name : string ,
age: number ,
gender ? : string
}

let zhangsan : Person {
name : 'zhangsan' ,
age : 25
}

只读属性

通过 readonly 关键词来限定:

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

let zhangsan: Person = {
name: 'Tom',
age: 25,
id: 1,
};

键名为多种类型

比如这样的数据(键名可能为 string,也可能为 number):

1
2
3
4
{
2021: 123,
'2022': 456,
}

可以这样定义:

1
2
3
4
{[key: string | number]: number | string }

// 也可以用Record
Record<string|number, string>

高级类型

泛型

<T>

泛型就是当你定义的时候,不确定后面真正使用的时候会是什么数据类型。泛型可以极大增强程序的扩展性。

<T>这里的 T 是 type 的缩写

泛型函数的结构

泛型函数一般按照这样的顺序来编写:

1
2
3
4
5
6
7
function 函数名<定义泛型参数>(函数参数): 函数返回类型 {
函数主体;
}

const 函数名 = <定义泛型参数>(函数参数): 函数返回类型 {
函数主体;
}

一个案例:

1
2
3
4
5
6
function functionName<T>(age: T): T {
console.log(age);
}
const functionName = <T>(age: T): T {
console.log(age);
}

定义与使用

泛型的应用,包括了定义使用两部分。在尖括号中的是定义,后面的是使用,比如:

1
2
3
4
5
6
7
// <T, U>是定义泛型参数
// [T, U]和[U, T]是使用泛型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

约束

这是为了限制参数类型,便于我们在函数内部可以使用特定类型的特定属性/方法(比如下面的 length 属性)。

泛型参数的类型,通过 extends 关键词进行约束:

1
2
3
4
5
6
7
8
9
10
// 通过interface定义类型
interface Lengthwise {
length: number;
}

// 约束泛型参数的数据类型,其一定有length属性,且其值是个number
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

再看一个例子:

1
2
3
4
function loggingIdentity<K extends keyof T>(arg: K): T {
console.log(arg.length);
return arg;
}

这个 K extends keyof T,应该是说 key 被约束在 T 的 key 中,不能超出这个范围,否则会报错的。

在泛型中使用 extends 并不是用来继承的,而是用来约束类型的。

PS: 泛型可以大量应用到我们的可视化组件中。

比如下面这样:

1
2
3
4
5
// 泛型参数Opts的类型是BaseBarSeriesOption<unknown>
// 泛型参数Opts的默认值是BaseBarSeriesOption<unknown>
class BaseBarSeriesModel<
Opts extends BaseBarSeriesOption<unknown> = BaseBarSeriesOption<unknown>
> extends SeriesModel<Opts> {}

映射类型

{ [K in keyof Type]: Transform<Type[K]> }

条件类型

T extends U ? X : Y

工具类型

内置的高级类型辅助工具

typescript 工具类型 Partial、Pick、Omit - Reminisce* - 博客园

Partial

将 Type 的所有属性设置为可选。

将已声明的类型中的所有属性标识为可选的。

1
const usePartial: Partial<Person> = {};

Required

将 Type 的所有属性设置为必须。

Readonly

将 Type 的所有属性设置为只读。

Record<Keys, Type>

构造一个对象类型,其属性键是 Keys,属性值是 Type。

Pick<Type, Keys>

从 Type 中选取一组属性 Keys。

从一个复合类型中,取出几个想要的类型的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原始类型
interface TState {
name: string;
age: number;
like: string[];
}
// 如果我只想要name和age怎么办,最粗暴的就是直接再定义一个(我之前就是这么搞得)
interface TSingleState {
name: string;
age: number;
}
// 这样的弊端是什么?就是在Tstate发生改变的时候,TSingleState并不会跟着一起改变,所以应该这么写
interface TSingleState extends Pick<TState, 'name' | 'age'> {}

比如 ZRender 中的应用:

1
2
3
4
5
export type ElementStatePropNames =
| (typeof PRIMARY_STATES_KEYS)[number]
| 'textConfig';
export type ElementState = Pick<ElementProps, ElementStatePropNames> &
ElementCommonState;

Omit<Type, Keys> (忽略、删除)

从 Type 中排除一组属性 Keys。

从声明的类型中删除指定的属性生成新的类型。

1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Omit 方法中使用了ExcludePick方法。结合一个小例子理解这个方法。

1
2
3
4
5
type OmitPerson = Omit<Person, 'name'>;
// 第一步执行Exclude方法,Exclude<keyof Person, 'name'>, 等价于 'age' | 'hobby'
// 第二步执行Pick方法,Pick<Person, 'hobby' | 'age'> 从类型Person中选择 'age' | 'hobby'属性
// 因此type OmitPerson = {age: number, hobby: string }
const useOmit1: OmitPerson = { age: 10, hobby: 'swim' };

Exclude<Type, ExcludedUnion>

从 Type 中排除在 ExcludedUnion 中的类型。

排除不需要的属性,最后的到的是排除后的属性,常用来配合其他的方法使用。

和 Pick 的作用相反。

1
2
3
4
type PersonKeys = keyof Person;  // "name" | "age" | "hobby"

// 等价于 type Age = "age
type Age = Exclude<PersonKeys, 'name' | 'hobby'>"

Extract<Type, Union>

从 Type 中提取所有可以赋值给 Union 的类型。

ReturnType

获取函数 Type 的返回类型。

InstanceType

获取构造函数类型 Type 的实例类型。

用户自定义类型

接口

interface

相比 Java,还能定义属性,也能定义方法

接口可以定义重名的,会自动合并,常用于(也仅用于)扩展现有接口;这个设计存疑

–TS 不让通过 Date.prototype.xxx 去扩展现有内容,只能通过接口扩展的方式实现

接口也可以继承接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person{
name: string;
age?: number;
// 任意属性
[propName: string]: any;
// 只读属性
readonly sex: string;
}

let zhou: Person = {
name: "zhou",
score: 3,
sex: "male"
}

类型别名

type

1
type pos = [number, number];

这个非常有用,比如我们的可视化组件对入参的检查,就可以用 type。

一些常用的交叉类型:

1
2
3
type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

类型操作符和关键字

typeof 类型操作符

typeof 操作符属于 类型查询 的一种。类型查询用于获取一个变量或对象的类型。这在 TypeScript 中是一种常用的方法,用于从已有的变量中推导出类型。

例如,如果你有一个已经定义的对象或变量,你可以使用 typeof 来获取它的类型,并将这个类型赋值给另一个变量或者用作其他类型的定义。
typeof 在 TypeScript 中主要用于类型推断和重用已有变量或对象的结构作为类型,这有助于减少冗余代码并提高类型系统的一致性。

keyof 类型操作符

keyof 类型操作符用于获取某个类型的所有键名作为联合类型。例如,如果你有一个类型 { a: number; b: string; },那么 keyof 这个类型会产生'a' | 'b'这样的联合类型。

instanceof 关键字

虽然 instanceof 主要用于运行时检查对象是否为特定类的实例,但在类型保护中,它也可以用来缩小类型的范围。

类型断言

类型断言(如 as 关键字)用于手动指定一个更具体的类型。例如,someVar as string 告诉 TypeScript someVar 应该被视为 string 类型。

类型保护函数

类型保护通过定义一个函数来检查一个变量是否属于某个特定类型。例如,function isString(test: any): test is string { return typeof test === “string”; }。

条件类型

条件类型(TypeA extends TypeB ? X : Y)用于根据类型关系选择类型。它们在高级类型构建时非常有用。

泛型类型参数:泛型(如 )用于定义灵活的类型,它们可以在多个位置以不同的方式被使用。

基本概念

类型推断

TS 可以自动推断类型,帮助我们简化编码;但是一般不推荐用类型推断,我们应该明确指定变量类型。

类型断言

应用第三方东西的时候很有用,可以作为 TS 和第三方衔接的一个桥梁。

可以用尖括号<>,也可以用as关键词:

1
const div = document.getElementById('abc') as HTMLElement;
1
2
3
4
5
6
7
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}

function

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
function sum(a: number, b: number): number
{
return a + b
}

let sum1 = function (a: number, b: number): number {
return a + b
}

// 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
let sum2: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y
}


/**
* 通过接口定义函数的形状
* 注意:TS的接口和常规的接口,有很大的不同
* TS的接口细化到了某个函数、某个JSON对象的定义
*/
interface SearchFunc{
(source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = function (source: string, subString: string): boolean {
return source.search(subString) !== -1
}


// 可选参数
function sum3(a: number, b: number, c?: number): number
{
let sum: number = a + b
if (c) {
sum += c
}
return sum
}

// 参数默认值
// TypeScript 会将添加了默认值的参数识别为可选参数
function sum4(a: number, b: number, c: number = 3): number
{
let sum: number = a + b
if (c) {
sum += c
}
return sum
}

// 剩余参数
function sum5(a: number, b: number, ...numbers: number[]): number
{
let sum: number = a + b
if (numbers) {
for (let i = 0; i < numbers.length; i ++) {
sum += numbers[i]
}
}
return sum
}

// 重载
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}

命名空间 namespace

1
declare namespace xxx

声明文件

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 声明文件
*
* 目的:当使用第三方库时,我们需要引用它的声明文件,才能获得对应的【代码补全】、【接口提示】等功能。
*
* 声明文件的安装:
* npm install @types/jquery --save-dev
*
* 搜索声明文件的网站:
* https://microsoft.github.io/TypeSearch/
*
* 比如Node开发,要引入npm install @types/node --save-dev
*/

如果程序中使用了第三方的库,比如 jQuery,也需要进行声明文件的编写,否则会提示变量 jQuery 不存在。

typeof

typeof 可以获取一个具体数据实例的数据结构。

这在提取第三方数据或者接口的数据结构时很有用。

(TODO)类型查找

多用于第三方接口的使用;自己写的还是全部用定义

keyof

获取数据定义的键名。

比如 on()事件的案例:我们想根据传入的不同的事件类型(PC 端是 click,移动端是 move),进行不同的第二个参数的提示:

1
function on<K extends keyof EventParam>(key: K, callback(e:EventParam[K]) => void)

应用示例:

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
export type StateDefinition = {
[k: string]: unknown;
material: { [k: string]: unknown };
};

export default class Element<
O extends Object3D = Mesh,
S = { [k: string]: StateDefinition }
> {
/** 状态与样式的解耦;行为与表现相分离 */
useState(stateName: keyof S) {
if (this.isNull()) {
return;
}

const stateObject =
this?._stateProxy?.(stateName, this.currentStates) ||
this._states[stateName] ||
({} as S[keyof S]);

this.currentStates = [stateName] as unknown as keyof S[];

this._applyStateObject(stateObject);
}
}

缺省(?)与不可或缺(!)

变量后面加问号是缺省

变量后面加惊叹号是肯定存在

(TODO)装饰器模式

给第三方类扩展属性

参考这个文章:https://blog.csdn.net/palmer_kai/article/details/107687717

注意增加的.d.ts 文件,是不需要主动 import 的,直接就会识别加载。

另外这个文章顶部从顶级模块引入了需要扩展的子模块,实际上是不需要引入的,参考我扩展 three.js 的 Group 案例,就没有 import Group 进来:

1
2
3
4
5
6
7
8
9
10
import { Node } from './type/Data';

/**
* 扩展自定义的节点属性
*/
declare module 'three' {
export interface Group {
node: Node;
}
}

然后发现有新问题:我想使用原始的 Group 功能的时候,会报错:**”Group”仅表示类型,但在此处却作为值使用。**

1
2
const helpers = new Group();
// 报错:"Group"仅表示类型,但在此处却作为值使用。

没找到好的扩展 Class 的方法,最终我选择重新定义 Group 这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Object3D } from 'three';
import { Node } from './type/Data';

/**
* 扩展自定义的节点属性
*/
declare module 'three' {
export class Group extends Object3D {
constructor();
type: 'Group';

readonly isGroup: true;

node: Node;
}
}

参考资料:JS 模块增强

一些 Q&A

type 和 interface 的区别

TypeScript type 和 interface 区别_天渺工作室的博客-CSDN 博客_typescript 中 type 和 interface 的区别

typeinterface 都用于定义类型,但它们之间存在一些关键的区别和使用场景:

  1. 扩展性

    • interface  可以被无限次地扩展(extended),并且可以在多个地方声明后合并为一个接口。
    • type  只能被扩展一次,不能在多个地方声明合并。
  2. 原始类型别名

    • type  可以用来给原始类型创建别名,例如  type Name = string;
    • interface  不能用来给原始类型创建别名,它只能用于对象类型。
  3. 交叉类型

    • type  可以用来创建交叉类型,例如  type Person = { name: string } & { age: number };
    • interface  也可以通过  extends  关键字实现类似的功能,但语法不同,例如  interface Person extends Name, Age { }
  4. 重声明

    • interface  可以被重声明,并且重声明的接口会合并。
    • type  重声明会报错,因为 TypeScript 会将它们视为不同的类型。
  5. 索引签名

    • interface  可以包含索引签名,例如  interface StringArray { [index: number]: string; }
    • type  也可以包含索引签名,但是使用  type  时,索引签名必须是对象类型的一部分。
  6. 构造签名

    • type  可以为函数或构造函数创建一个签名,例如  type Newable = new (...args) => Instance;
    • interface  也可以定义构造函数签名,但是通常用于类或对象的上下文中。
  7. 类型守卫

    • type  可以用于类型守卫,例如使用  typeof  或  instanceof  操作符。
    • interface  通常不用于类型守卫。
  8. 命名约定

    • interface  通常用于定义对象的形状,遵循 PascalCase 命名约定。
    • type  可以用于更广泛的类型定义,包括联合类型、元组、枚举成员等,命名约定更灵活。

总结来说,interface 更适合用于定义“结构契约”,而 type 提供了更广泛的类型系统功能,包括原始类型别名、交叉类型、更复杂的类型操作等。在实际开发中,根据具体需求和场景选择合适的类型定义方式。

any VS. unknown

https://www.zhihu.com/question/355283769/answer/2136229141

  • any 相当于直接暴力通过类型检查,打这张牌要警惕。
  • unknown 是所有类型的父类型(top type),想强行 as 的时候更建议打这张牌。

as 原则上只能转换存在子类型关系的两个类型,且一次只能转换一层

any 能最简单暴力地解决问题,所以初学者普遍打 any 牌不打 unknown 牌。

在 TS 这样支持 interface 组合的语言里,业务逻辑中的类型所构成的应当不是树而是个有向无环图,子类型关系也可以视为一种偏序关系。像这样:

jc

TS 类型体操入门:

https://zhuanlan.zhihu.com/p/384172236

常见问题

编译报错

如何忽略编译报错?

有时候我们修改老项目,紧急发布的时候没时间一个一个编译问题都改掉,需要忽略编译报错。

单文件中,可以通过如下注释来处理:

1
2
3
4
5
6
7
8
// @ts-ignore
// 单行忽略

// @ts-nocheck
// 忽略全文

// @ts-check
// 取消忽略全文

如果想全局设置,可以修改 tsconfig.json,增加这 2 项:

1
2
3
4
5
6
7
{
"compilerOptions": {
"strict": true,
// 跳过第三方库的类型检查,不然Build会报错
"skipLibCheck": true
}
}

TypeScript 版本过低导致的报错

datav-server 项目中,本地开发没问题,部署到服务器上,tsc 编译就报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
node_modules/@types/express-serve-static-core/index.d.ts:1163:17 - error TS1005: ';' expected.

1163 /** Enable `setting`. */
~~~~~~~

node_modules/@types/express-serve-static-core/index.d.ts:1166:18 - error TS1005: ';' expected.

1166 /** Disable `setting`. */
~~~~~~~

node_modules/@types/express-serve-static-core/index.d.ts:1170:31 - error TS1005: ';' expected.

1170 * Render the given view `name` name with `options`
~~~~

node_modules/@types/express-serve-static-core/index.d.ts:1170:48 - error TS1005: ';' expected.

1170 * Render the given view `name` name with `options`
~~~~~~~

node_modules/@types/express-serve-static-core/index.d.ts:1186:16 - error TS1005: ';' expected.

去这个包的 issue 中找了下,看到有类似问题:

https://github.com/DefinitelyTyped/DefinitelyTyped/issues/53397

可能和我们 ts 版本过低有关系,package.json 中是:

1
"typescript": "^3.0.0"

打印了版本看:

1
2
3
4
tsc -v

# 输出
Version 3.9.10

然后通过升级项目下的 package.json 中的 TypeScript 的版本进行了解决。

TS7006: Parameter ‘_‘ implicitly has an ‘any’ type.

修改 tsconfig.json,将noImplicitAny设置为false

Cannot find name ‘Map’. Do you need to change your target library? Try changing the ‘lib’ compiler option to ‘es2015’ or later.

第一种方法:

修改 tsconfig.json,添加 lib 配置项:

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "CommonJS",
"target": "es5",
"allowJs": true,
"lib": ["ESNext"]
}
}

第二种方法:

1
npm install -D @types/node

TS2304: Cannot find name ‘HTMLDivElement/window/document’

修改 tsconfig.json,给 lib 配置项添加 DOM 依赖:

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "CommonJS",
"target": "es5",
"allowJs": true,
"lib": ["ESNext", "DOM"]
}
}

为什么之前运行正常的 egg.js 项目,过一段时间就会编译报错?

一开始怀疑是因为项目提交了 package-lock.json,部分包过期了,后来发现不是这个原因(这个逻辑上也说不通)

和 node 版本有关,
报错的环境:v8.9.3

升级到:v12.18.3

TS 解释器在不同的 Node 版本下,行为/能力不一致。

循环依赖

在只需要使用定义的文件中,引入时加上type:

1
import type BarChart from '../chart/BarChart';

Error: ‘ChartOption’ is not exported by src/chart/util/structure.ts, imported by src/chart/spiral/Chart.ts

如果你看自己程序中已经确实 export 了,但是仍然报这个错误,那么可能也是因为循环依赖导致的,加上 type 即可。

未声明时使用类

将类名改为 this:

1
2
3
4
5
6
7
8
9
// 报错
class BarChart extends BaseChart {
xPeriodScale: XPeriodScale<BarChart>;
}

// 正常
class BarChart extends BaseChart {
xPeriodScale: XPeriodScale<this>;
}

export 的 interface 找不到

报类似这样的错误:

“export ‘state’ (imported as ‘stateModel’) was not found in ‘../store/state.ts’

解决方案就是:单独用一个文件导出 interface:

https://github.com/angular/angular-cli/issues/2034#issuecomment-302666897

node_modules/@types/express-serve-static-core/index.d.ts:917:14 - error TS1005: ‘;’ expected.

完整报错类似这样:

1
2
node_modules/@types/express-serve-static-core/index.d.ts:917:14 - error TS1005: ';' expected.
917 * - `path` defaults to "/"

其实并不是因为缺少分号,而是因为项目引用的 TS 版本过低,无法识别模板字符串导致的。

解决方案:升级项目依赖的 TS 版本即可。

TypeError: {(intermediate value)(intermediate value)(intermediate value)} is not a function

这是因为某一个语句之后缺少分号,导致无法正常识别语法,比如类似这样的:

1
2
3
4
5
6
7
const config = {
name: '张三',
}(
// 此处缺少分号,会导致将下面的语句连在一起,进而无法识别

this.mysql as any
).query();

解决方案很简单,加个分号即可。

error TS1192: Module ‘“http”‘ has no default export

一些系统自带的包报错,这是因为 http 模块没有默认导出。事实上,http 模块的确没有默认导出。因为 http 是遵循 cjs 规范写的。即类似于这种导出:

1
2
3
4
5
module.exports = http = {
method1,
method2,
...
}

解决方案:

修改 TS 的配置,加上这个:

1
2
3
4
5
{
"compilerOptions": {
"esModuleInterop": true
}
}

给第三方类扩展属性时,提示“扩大中的模块名称无效。模块“THREE”解析到位于 处的非类型化模块,其无法扩大”

需要把 import 放在 declare 内部:

1
2
3
4
5
6
declare module 'THREE' {
import * as THREE from 'three';
export interface THREE {
onEvent: () => void;
}
}

延迟赋值的属性,在调用时提示可能为空的问题

通过**非空断言(!)**解决。

可以在定义属性类型的时候添加,比如这样:

1
2
3
4
5
6
class Chart {
scene!: Scene;
constructor(dom: HTMLDivElement, option: ChartOption) {
// 这样在构造方法中,就不用初始化该属性了
}
}

也可以在调用的地方使用,比如这样:

1
this.scene!.add(obj);

给部分属性赋予默认值

比如这样一个结构:

1
2
3
4
5
6
7
8
const data: {
date: string;
open: number;
high: number;
low: number;
close: number;
change: number;
}[] = [];

在 Typescript 中,接口只是描述对象的外观,不能包含任何函数。因此,您不必像使用类那样构造接口。正因为如此,你不能在接口中进行任何初始化。

子类重新定义属性后,constructor 中直接使用父类赋值的属性报 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Father {
name: string;
constructor(name: string) {
this.name = name;
}
}

class Child1 {
name: string; // 明确重新定义了该属性
constructor(name: string) {
super(name);
console.log(this.name); // 输出undefined
}
}

class Child2 {
constructor(name: string) {
super(name);
console.log(this.name); // 可以正常获取值
}
}

所以想要使用父类的属性,可以有两个方法:

1、子类中不要定义该属性

2、父类定义时,使用泛型

子类属性的初始化时机-父类调用子类属性报 undefined 的问题

这个和 TS 没关系,是 JS 的特性。

父类:

1
2
3
4
5
6
7
class Father {
constructor() {
this.init();
}

init() {}
}

子类:

1
2
3
4
5
6
7
8
9
10
class Child extends Father {
name = 'child';

init() {
// 输出undefined
console.log(this.name);
}
}

new Child();

目前采用的解决方法是将子类的该属性设置为static属性:

1
2
3
4
5
6
7
8
9
10
class Child extends Father {
static name = 'child';

init() {
// 输出child
console.log(Child.name);
}
}

new Child();

引入的第三方库,明明声明了定义,但是仍然报错

比如这次用 llama index,就遇到这个问题:

1
2
3
4
const prompt = new PromptTemplate({
template: raw_prompt[0].prompt,
templateVars: raw_prompt[0].variable,
});

报错:

error TS2554: Expected 0 arguments, but got 1.

问题原因尚未查明,然后临时先通过覆盖定义的方式解决了,即写了个/projectPath/typings/app/llamaindex.d.ts:

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
// llamaindex.d.ts
import { PromptType } from 'llamaindex';

declare module 'llamaindex' {
interface PromptTemplateOptions<
TemplatesVar extends readonly string[],
Vars extends readonly string[],
Template extends StringTemplate<TemplatesVar>
> {
template: Template;
templateVars: Vars;
promptType?: PromptType;
}

class PromptTemplate<
TemplatesVar extends readonly string[] = string[],
Vars extends readonly string[] = string[],
Template extends StringTemplate<TemplatesVar> = StringTemplate<TemplatesVar>
> extends BasePromptTemplate<TemplatesVar, Vars> {
#template: Template;
promptType: PromptType;

constructor(options: PromptTemplateOptions<TemplatesVar, Vars, Template>);
}
}

装饰器的 return 类型和被装饰方法的 return 类型不匹配的问题

1
2
3
4
5
6
7
@wrapLLMEvent
async chat(
params: LLMChatParamsStreaming | LLMChatParamsNonStreaming,
): Promise<AsyncIterable<ChatResponseChunk> | ChatResponse<object>> {
if (params.stream) return this.streamChat(params);
return this.nonStreamChat(params);
}

error TS1270: Decorator function return type ‘(this: LLM<object, object>, params: LLMChatParamsStreaming<object, object> | LLMChatParamsNonStreaming<object, object>) => Promise<…>’ is not assignable to type ‘void | TypedPropertyDescriptor<{ (params: LLMChatParamsStreaming<object, object>): Promise<AsyncIterable<ChatResponseChunk>>; (params: LLMChatParamsNonStreaming<…>): Promise<…>; }>’.

TS5069: Option ‘declarationMap’ cannot be specified without specifying option “declaration’ or option ‘composite’.

使用storybook开发KAmis组件的时候出现的问题。
据说是TS版本的问题,通过升级/降级解决:
https://github.com/cypress-io/cypress/issues/30016
https://github.com/ng-packagr/ng-packagr/issues/1464

技巧

将复杂的数据类型,都定义为 interface

优化前:

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
formateData(
dataArr: []
): { date: string; open: number; high: number; low: number; close: number; change: number }[] {
const data: {
date: string;
open: number;
high: number;
low: number;
close: number;
change: number;
}[] = [];
dataArr.forEach((datum, index) => {
data.push({
date: datum[0],
open: parseFloat(datum[1]),
high: parseFloat(datum[2]),
low: parseFloat(datum[3]),
close: parseFloat(datum[4]),
change:
index > 0
? parseFloat(dataArr[index][4]) - parseFloat(dataArr[index - 1][4])
: parseFloat(dataArr[index][4]) - parseFloat(dataArr[index][1]),
});
});
return data;
}

优化后:

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
// 通过定义interface来管理数据结构
interface Daily {
date: string;
open: number;
high: number;
low: number;
close: number;
change: number;
}

formateData(
dataArr: []
): Daily[] {
const data: Daily[] = [];
dataArr.forEach((datum, index) => {
data.push({
date: datum[0],
open: parseFloat(datum[1]),
high: parseFloat(datum[2]),
low: parseFloat(datum[3]),
close: parseFloat(datum[4]),
change:
index > 0
? parseFloat(dataArr[index][4]) - parseFloat(dataArr[index - 1][4])
: parseFloat(dataArr[index][4]) - parseFloat(dataArr[index][1]),
});
});
return data;
}

定义文件单独编写

思考:这个是不是和 C++很像?是否有共通之处?

定义文件一般都比较长,放在具体类里面,会让类文件很长。可以考虑独立用一个文件存放。

包括:

  • 参数对象的定义
  • 常量定义

文件名大小写问题

类大写,工具函数文件小写

不要定义可选类型|

如果定义了可选类型,每次都要加感叹号(!),很不优雅,编辑器也会出现黄色提示。

如果该属性是个数组,那就定义为数组,然后没数据的就给个空数组好了。

如果是简单对象,就给个默认值,比如 0 之类的。

原则:数据结构应该是固定的,不可变的。

工具

(精)可视化展示继承关系

https://tsdiagram.com/

这个的代码也值得一看

TypeScript类型图解

https://types.kitlangton.com/

参考资料

入门教程文档:

http://ts.xcatliu.com/

你不知道到的 TS 泛型:

https://segmentfault.com/a/1190000022993503

通过 webpack 编译构建 TS:

https://www.jianshu.com/p/f6917e257b7a

用 unknown 替代 any:

https://www.jianshu.com/p/516fe7cbc9e8

官方文档:

https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html

TypeScript 最近各版本主要特性总结:

https://blog.csdn.net/qq_42427109/article/details/130533706

TypeScript 5 与 TypeScript4 区别:

https://www.toimc.com/typescript-5-vs-4/

优秀的 TS 项目-React 状态管理库:

https://github.com/pmndrs/zustand/

(TODO)type-challenges

https://github.com/type-challenges/type-challenges

在线版:
https://typeroom.cn/problems/all

好玩的项目

太空射击游戏:

https://itnext.io/building-a-space-shooter-game-with-html5-canvas-typescript-part-1-20663025c7f5

代码案例

参数类型的继承

1
class EventEmitter<EvtDef extends EventDefinition = EventDefinition>

EvtDef 是泛型参数的名称,它后面跟着 extends EventDefinition 表示 EvtDef 可以是任何继承自 EventDefinition 的类型,或者直接就是 EventDefinition 类型。泛型允许你创建可重用和灵活的组件。

  • extends EventDefinition: 这指定了 EvtDef 必须继承自 EventDefinitionEventDefinition 可能是一个接口或者抽象类,定义了事件的基本结构或行为。

  • = EventDefinition: 这是泛型参数的默认类型。这意味着如果在使用 EventEmitter 类时没有指定类型参数,则默认使用 EventDefinition 类型。

事件处理器映射

1
type EventDefinition = Record<string, (...args: unknown[]) => unknown>;

EventDefinition 被定义为一个对象类型,其中键(key)是字符串类型,值(value)是一个函数。这个函数可以接收任意数量的参数(参数数组 args),每个参数的类型是 unknown,并且这个函数的返回值类型也是 unknown

这种类型通常用于定义事件处理器映射,其中每个事件名称(字符串)映射到一个事件处理器函数。这个函数可以接收任意的参数,并执行一些操作,但其返回值的类型没有具体限制。

例如:

1
2
3
4
5
6
7
8
9
10
const eventHandlers: EventDefinition = {
click: (x: number, y: number) => {
console.log(`Clicked at (${x}, ${y})`);
return 'Event handled'; // 返回值类型为 unknown
},
hover: (item: any) => {
console.log(`Hovered over ${item}`);
// 返回值类型为 unknown,这里没有返回值
},
};

导出类型别名

1
2
3
4
5
6
7
8
export type Query<H extends EventDefinition[string] = EventDefinition[string]> =

| {
[Key in keyof Parameters<H>[0]]:
| Parameters<H>[0][Key]
| Parameters<H>[0][Key][];
}
| ((...args: Parameters<H>) => boolean);

这段代码是 TypeScript 中的一个导出类型别名 Query 的声明,它使用了高级的泛型和类型映射特性。让我们逐步解析这个声明:

  1. export type Query<H extends EventDefinition[string] = EventDefinition[string]>: 这是类型别名的声明,Query 是别名的名称,它是一个泛型类型,其中 H 是一个泛型参数,它默认继承自 EventDefinition[string]。这里的 EventDefinition[string] 指的是 EventDefinition 类型中作为值的函数类型。

  2. = EventDefinition[string]: 这是 H 的默认类型参数,意味着如果使用 Query 时没有显式指定 H,则默认使用 EventDefinition 中的函数类型。

  3. |: 这是 TypeScript 中的联合类型操作符,表示类型可以是左边类型 A 或者右边类型 B

  4. 左边类型 { [Key in keyof Parameters<H>[0]]: ... }:

    • 使用了高级类型  Parameters  和  keyofParameters<H>  获取  H  函数的所有参数类型组成的元组类型。
    • keyof Parameters<H>[0]  获取  Parameters<H>  中第一个参数类型的所有键(属性名)。
    • Parameters<H>[0][Key] | Parameters<H>[0][Key][]  表示每个属性  Key  的类型是  Parameters<H>[0]  中相应属性的类型或者是该类型的数组。
  5. 右边类型 ((...args: Parameters<H>) => boolean): 这是一个函数类型,接受一个参数列表 args,这个参数列表的类型与 H 函数的参数类型相同,函数返回一个布尔值。

综合来看,Query 类型别名定义了一个类型,可以是:

  • 一个对象,其键是  H  函数第一个参数类型属性的名称,值是该属性类型的值或者该类型的数组。
  • 或者是一个函数,接受与  H  函数相同的参数,并返回一个布尔值。

例如,如果 EventDefinition 像前面的例子一样定义,并且有一个事件处理器:

1
2
3
4
type EventDefinition = {
click: (x: number, y: number) => void;
hover: (item: string) => void;
};

那么 Query<EventDefinition['click']> 可以是:

1
2
3
4
{
x: number | number[];
y: number | number[];
} | (...args: [number, number]) => boolean;

这意味着 Query 可以是一个对象,有 xy 属性,属性值可以是数字或者数字数组;或者是一个接受两个数字参数并返回布尔值的函数。

以下是一些具体的例子,展示了 Query 类型在不同场景下的应用:

参数对象类型定义

假设我们有一个函数,它需要一个配置对象来设置不同的选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type EventDefinition = {
setup: (config: { width: number; height: number }) => void;
};

// 使用 Query 来定义参数类型
export type SetupQuery = Query<EventDefinition['setup']>;

function setup(setupConfig: SetupQuery): void {
if (
typeof setupConfig.width === 'number' &&
typeof setupConfig.height === 'number'
) {
// 正常执行设置
console.log(
`Setting up with width: ${setupConfig.width} and height: ${setupConfig.height}`
);
} else {
// 参数验证失败
throw new Error('Invalid configuration object');
}
}

// 使用
setup({ width: 800, height: 600 });

函数重载

使用 Query 实现函数重载,接受单个参数对象或多个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type EventDefinition = {
process: (data: { item: string; value: number }) => boolean;
};

export type ProcessQuery = Query<EventDefinition['process']>;

function process(data: ProcessQuery): boolean {
if (typeof data === 'function') {
return data();
}
// 假设这里有复杂的处理逻辑
return data.value > 0;
}

// 使用单个参数对象
process({ item: 'test', value: 10 });

// 使用函数重载
process((item: string, value: number) => value > 0);

数据验证

使用 Query 来验证函数参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type EventDefinition = {
validate: (data: { id: string; content: string[] }) => boolean;
};

export type ValidateQuery = Query<EventDefinition['validate']>;

function validate(data: ValidateQuery): boolean {
if (typeof data.id === 'string' && Array.isArray(data.content)) {
return true; // 数据有效
}
return false; // 数据无效
}

// 使用
console.log(validate({ id: '123', content: ['item1', 'item2'] }));

事件处理

定义事件处理器,使用 Query 来确保事件数据的正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type EventDefinition = {
onClick: (event: { x: number; y: number }) => void;
};

export type ClickQuery = Query<EventDefinition['onClick']>;

function handleEvent(event: ClickQuery): void {
if (typeof event.x === 'number' && typeof event.y === 'number') {
console.log(`Click at (${event.x}, ${event.y})`);
}
}

// 模拟点击事件
handleEvent({ x: 100, y: 200 });

中间件或装饰器

定义一个中间件,使用 Query 来确保请求对象的类型正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type EventDefinition = {
authenticate: (req: { userId: string; token: string }) => boolean;
};

export type AuthenticateQuery = Query<EventDefinition['authenticate']>;

function authenticateMiddleware(req: AuthenticateQuery): boolean {
// 假设这里有认证逻辑
return req.token === 'valid-token'; // 简单示例
}

// 使用中间件
console.log(
authenticateMiddleware({ userId: 'user123', token: 'valid-token' })
);

这些例子展示了 Query 类型在不同场景下的应用,包括参数验证、函数重载、事件处理等。通过使用 TypeScript 的高级类型特性,我们可以创建类型安全且灵活的代码。