npm i typescript -g
使用 tsc 命令编译 ts 文件生成 js 文件,再通过 node 运行 js 文件
tsc filename
npm i ts-node -g
ts-node filename
tsconfig.json 是 TypeScript 项目的配置文件。通过手动创建或者使用
tsc -init
命令创建tsconfig.json
文件。
let isDone: boolean = true;
let isString: boolean = false;
let decLiteral: number = 6; // 十进制 decimal
let hexLiteral: number = 0xf00d // 十六进制 hexadecimal 0x开头表示十六进制
let binaryLiteral: number = 0b1010 // 二进制 Binary 0b开头表示二进制
let octalLiteral: number = 0o744 // 八进制 octal 0o开头表示八进制
let str: string = 'abc'
str = 'name'
let str1: string = `${str}efg`
// 1 元素类型后面接上 [] 表示由此类型元素组成的一个数组
const list: number[] = [1, 2, 3];
const list1: (number | string) [] = [1, 2, 3, '4'];
// 2 Array<元素类型>
const list2:Array<number> = [1, 2, 3];
// 3 ReadonlyArray<元素类型> 只读数组类型
const list3:ReadonlyArray<number> = [1, 2, 3]; // 这里不做多介绍,interface 部分会具体介绍
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number] = ['abcdefg', 111];
// 默认情况下枚举从0开始为元素下标,并且按顺序自增。也可以手动的指定下标
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
console.log(c) // 1
// 手动指定元素开始的编号
enum Color1 {Red = 1, Green, Blue}
let g: Color1 = Color1.Green;
// console.log(g) // 2
// 使用字符串作为枚举值,使用字符串之后的元素必须要有初始值,否则会报错
enum Color1 {
Red = 2,
Green,
Blue = 'blue',
// White, 会报错
}
let color: Color1 = Color1.Green;
let color1: Color1 = Color1.Blue;
console.log(color, color1); // 3, blue
// 通过枚举的值来查找这个值在这个枚举里映射的key (只能通过number的下标获取,string的会报错)
let colorName: string = Color1[3];
// let colorName1: string = Color1['blue']; // ts 代码不会提示报错
console.log(colorName); // Green
在 TypeScript 中,任何类型都可以被归为
any
类型。这让any
类型成为了类型系统的顶级类型(也被称作全局超级类型)。当我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查,那么我们可以使用any
类型来标记这些变量,不过any
也不能滥用,滥用的话也就失去了使用 TS 的意义了。
let notSure: any = 4;
notSure = 'abc';
notSure.toFixed(); // 代码不会提示错误
某种程度上来说,
void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是void
。
function warnUser(): void {
console.log('This is my warning message');
}
这两个类型的本身用处不是很大, 默认情况下
null
和undefined
是所有类型的子类型,例如:可以把null
和undefined
赋值给number
类型的变量。但是当你指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们自身。
let u: undefined = undefined;
let n: null = null;
let nu: number = 4;
nu = undefined; // 在 tsconfig 里把 strictNullChecks 设置为 false 则不会报错, strictNullChecks 为 true 时 undefined 只能赋值给 void 和自身,ts 官方建议开启 strictNullChecks 选项
let v: void;
v = undefined; // 开启了 strictNullChecks 也是 OK 的
v = null; // 但是 null 不能赋值给 void 类型,暂时不清楚原因,官网文档说是可以的
js
中通常 void 0
会用来获取 undefined
。undefined
可能会被重写,但是 void 0
返回的值一定会是 undefined
。猜测这就是为什么 undefined
可以赋值给 void
类型的原因吧。
never
类型表示的是那些永不存在的值的类型。例如,throw error
抛出异常的函数,死循环的函数。
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error('something failed');
}
function infiniteLoop(): never {
while (true) {}
}
unknown
类型是 TypeScript 3.0 引入了一个顶级的类型,任何类型都可以赋值给unknown
类型,但是unknown
类型除了赋值给any
和它本身之外,不可以赋值给其他类型。所以,unknown
比any
更安全。
let a: any = 1;
let b: unknown;
let num: number;
b = 'str'; // 任何类型都可以赋值给 unknown
b = a; // any 类型也可以赋值给 unknown
a = b; // unknown 类型只能赋值给 any 或者他自身
let d: unknown = 6.001;
d.toFixed(); // error; unknown 类型上不能执行任何操作
(d as number).toFixed() // 可以使用类型断言来操作
类型注解: 在声明变量的时候显式的声明它的类型
类型推断: 在 TS 中,有些没有明确指出类型的地方,会自动推断出值的类型
// 类型注解
let num: number;
num = 123;
// 类型推断
let s = 'string'; // TS 自动推断出 s 为 string 类型
let n; // 定义式没有赋值,TS 会自动推断该变量为 any 类型
多个类型的合并类型,只需要满足多种类型中的一种;可以把
|
理解成js
中的||
(或)。
let temp: string | number;
temp = 123;
temp = 'temp';
let arr: (string | number)[];
arr = [1, 2, 3, '4'];
let getName: (name: string | undefined) => string = (name) => {
return name === undefined ? 'default' : name;
}
console.log(getName('tom')); // tom
console.log(getName(undefined)); // default
使用
type
关键字自定义一个类型,和interface
功能类似,interface
类型不能定义基本类型。
type customType = number | string;
let getTotal = (total: customType): number => {
return typeof total === 'number' ? total : parseInt(total, 10);
};
多种类型的集合,需要满足多种类型中所有的类型;可以把
&
理解成js
中的&&
(与)。
type T1 = { name: string };
type T2 = { age: number };
type T3 = T1 & T2;
let student: T3 = {
name: 'tom',
age: 18,
};
let student1: T3 = {
name: 'nico',
}; // error
使用一个具体的值来当做一个变量的类型。
// 字面量类型
let hello: 'hello' = 'hello';
hello = 1; // error; 不能将类型“1”分配给类型“"hello"”
let flag: true = true;
当你比
TypeScript
更了解某个值的详细信息,很清楚的知道这里这个变量是这种类型时,就可以使用类型断言这种方式可以告诉编译器。
// 类型断言的两种写法
// 尖括号写法
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
// as 语法
let someValue1: any = 'this is a string';
let strLength1: number = (someValue as string).length;
// symbol 类型
let sym = Symbol('hello');
// Date 类型
let date = new Date();
//.....
// 一般接口的首字母都是大写
interface Person {
name: string;
}
const getPersonNmae = (obj: Person): string => {
return obj.name;
};
let obj = {
name: 'tom',
sex: 'male',
};
// 使用一个变量缓存接收一下,即使传入的对象有多余的属性 TS 也不会报错,只会检查那些必需的属性是否存在。
// 根据类型的兼容性,因为都具有 name 属性,故而可以用此法来绕开多余的类型检查。
getPersonNmae(obj); // ok;
getPersonNmae({name: 'job', sex: 'male',}); // error; sex 不在 Person 类型中
// 但当以一个字面量的方式直接传递给一个变量的时候,TS 会强校验
//类似下面这种直接给一个 Person 类型的变量赋值,Person 对象有严格的类型定义,所以就不能多参数或者少参数了;
let person1: Person = {name: 'tony', sex: 'male',}; // error; “sex”不在类型“Person”中
interface Person6 {
name: string // 接口可以使用 “,” 或者 “;” 结尾,也可以直接换行不写符号
readonly age: number // 只读属性
sex?: string // 可选属性
}
function personInfo(person: Person6): void{
person.name = 'tom';
person.age = 19; // error; age 属性是只读的
console.log(person.sex)
}
personInfo({name: 'jack', age: 18}) // ok
personInfo({name: 'facker', age: 20, sex: 'amle'}) // ok
除了
readonly
属性, 在数组中,TS
还提供了readonlyArray
类型,此类型将数组的所有可变方法去掉了,可以使用这个类型来确保一个数组创建后再也不会被修改, 但是可以重新被赋值。
let fixedArr: ReadonlyArray<Person6> = [
{
name: 'jack',
age: 18,
},
{
name: 'uzi',
age: 19,
sex: 'male',
}
]
const obj = {name: 'tom', age: 20};
fixedArr[0] = obj; // error; ReadonlyArray 类型索引签名仅允许读取
fixedArr.push(obj); // error; ReadonlyArray 类型上不存在属性“push”
fixedArr.length = 3; // error; 无法分配到 "length" ,因为它是只读属性
fixedArr = [];
console.log(fixedArr); // []
注意:readonly
声明的只读数组类型与ReadonlyArray
声明的只读数组类型,二者等价。
let arr1: readonly number[] = [1, 2, 3];
let arr2: ReadonlyArray<number> = [1, 2, 3];
arr1[0] = 0; // error
arr2[0] = 1; // error
arr1.push(4); // error
arr2.push(4); // error
了解了可选属性之后,如果一个对象有很多个属性都是不确定的,不可能给每个属性都定义成可选属性;所以,当我们不确定一个对象之后会添加什么属性时,可以用任意属性。也叫作索引签名的方式,只支持两种索引签名:字符串和数字。
interface Person7 {
name: string,
age?: number,
[propsName: string]: any, // key 可以取任何名字
}
const p7: Person7 = {
name: 'jack',
sex: 'male',
like: 'lol',
}
interface Person8 {
name: string, // error,类型“string”的属性“name”不能赋给“string”索引类型“boolean”
age?: number, // error, 类型“number | undefined”的属性“age”不能赋给“string”索引类型“boolean”
// [propsName: string]: boolean,
// 可以这样改造,任意属性的类型必须包含确定属性和可选属性中的所有类型
[propsName: string]: string | number | undefined,
}
const p8: Person8 = {
name: 'jack',
age: 18,
sex: 'male'
}
interface 不仅可以定义一个对象,还可以定义函数
// 普通函数
function getNum (a: number, b: number): number {
return a + b
}
// 箭头函数 1
const getNum1 = (a: number, b: number): number => a + b
// 箭头函数 2
const getNum2: (a: number, b: number) => number = (a, b) => {
return a + b
}
// 使用 interface 定义一个函数
interface GetNum {
func(a: number, b: number): number
}
const getNum3: GetNum = (a, b) => a + b
了解了接口之后,发现接口和类型别名的作用的差不多,那他们之间有什么区别呢?主要有四个区别;
1.type
支持定义基本数据类型,interface
不支持;
2.type
支持联合类型,interface
不支持;
3.interface
支持合并同名接口,type
不支持;
4.这两种类型的继承方式语法不同; interface 通过extends
来实现继承,type 通过&
来实现继承,也叫作交叉类型
// type 支持基本数据类型
type t = undefined;
// type 支持联合类型
type combineT = number | string;
// interface 支持声明合并;同一作用域下的多个同名接口会自动合并
interface A {
x: number
}
// 类型 "{ x: number; }" 中缺少属性 "y",但类型 "A" 中需要该属性
const numObj: A = { // error
x: 1
}
// 合并之后之前的定义的类型就会报错
interface A {
y: number
}
const numObj1: A = {
x: 1,
y: 2,
}
type B = {
x: number
}
// error; 标识符“B”重复
type B = {
y: number
}
// 继承方式
interface Person {
name: string
}
interface Student extends Person {
class: number
}
type Person1 = {
name: string
}
type Person2 = {
class: number
} & Person1
// interface 类型可以用 extends 继承 type
interface Teacher extends Person1 {
subject: string
}
// type 也可以用 & 继承 interface 类型
type Person3 = {
class: number
} & Person
const st: Student = {
name: '小鱼',
class: 6,
}
const p1: Person2 = {
name: '小鱼',
class: 6,
}
const tech: Teacher = {
name: '影流之主',
subject: '瞬狱影杀阵',
}
const p2: Person3 = {
name: '凯隐',
class: 5
}
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
看着官网的文档有点晦涩难懂,只能理解泛型是一个灵活可复用的类型,就类似 Vue
和 React
中组件的概念。可以先看个例子,看看泛型具体能解决什么问题。
function identity(arg: number): number { return arg; }
上面定义了一个 identity 函数,接收一个 number
类型的参数,返回一个 number
类型的值;现在有个需求,这个函数可能还接收 string
类型的参数,并且返回一个 string
类型的值。这时候你可能会使用联合类型来改造
function identity(arg: number | string): number | string { return arg; }
随着项目的迭代更新,现在这个函数可能又接收一个 boolean
类型的参数,返回一个 boolean
类型的值,你是不是又得在联合类型中增加一个类型。或者使用 any
类型,虽然使用 any
可以使 TS
不报错,但是 any
类型就导致了这个函数的不可控,因为any
可以是任何类型,会导致入参和出参的不一致,就很容易出现 bug
。这时候泛型
就派上用场了,他可以很容易的解决入参和出参保持一致
的需求。
function identity<T>(arg: T): T { return arg; }
let output = identity<string>("myString");
是的,你没有看错,就是这么简单;但是
该怎么去理解呢? T
在这里是类型变量,并不是一定要使用 T
这个字母你也可以使用其他的字母,它是一种特殊的变量,只用于表示类型而不是值,也可以把它理解为是一个占位符
。相当于在声明函数的时候,也声明了一个 T
的类型变量,或者把它理解成一个函数的变量参数,在调用这个函数的时候传入类型。
// 省略类型的传入,因为 TS 有类型推断,这里可以推断出入的值是一个 string
let output = identity("myString"); // 这种调用的写法和上面的写法是完全等价的
泛型的默认参数很简单,就和es6中函数参数的默认一样使用,不过只有在使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
interface IInfo< T = string >{
name: T
}
// 当使用泛型时没有在代码中直接指定类型参数这个默认类型就会起作用。
const objInfo: IInfo = {
name: 123 // error, 不能将类型“number”分配给类型“string”。
}
const objInfo1: IInfo = {
name: '影流之主'
}
const objInfo2: IInfo<number> = {
name: 123
}
泛型并不是只能定义一个类型的,还可以定义多个。现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组。
function reverseFn(tuple: [string, number]): [number, string] {
return [tuple[1], tuple[0]]
}
这样写的话是不是就很生硬,只能固定元组的类型,而不能自己控制传入的参数,使用泛型就可以很好的解决。
function reverseFn1<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
reverseFn1(['str', 6])
reverseFn1([[1, 2, 3], 'test'])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3moW7kQ-1659687092628)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/239979d59b7346848afddf444df179be~tplv-k3u1fbpfcp-watermark.image?)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLQ8V6TO-1659687092629)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b42fb9ffcea47b59585555289283853~tplv-k3u1fbpfcp-watermark.image?)]
通过鼠标悬停在调用方法上,可以更好的理解泛型,更像是一个函数的参数,你传递什么,他就是什么类型。
interface UserInfo<T, U>{
name: T,
age: U
}
function getUserInfo<T, U>(name: T, age: U): UserInfo<T, U> {
return {
name,
age,
}
}
const userInfo = getUserInfo('tom', 18);
知道了泛型这么好用,那我们该什么时候用泛型呢?
当函数、接口或类在多个地方使用该数据类型时。
其实可以把泛型理解为一个特殊变量,在写js代码时,当一个数据需要重复使用,你是不是就会想着定义一个变量去使用,一个地方修改,所有地方都生效,泛型也是这样的。
1. 确保属性存在:
有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。比如说我们想在下方函数中输出一下传入参数的 length
。
function identity6<T>(arg: T): T {
console.log(arg.length); // error, 类型“T”上不存在属性“length”。
return arg;
}
我们需要确保这个泛型必须有 length
属性,该怎么实现呢?
可以使用 interface
结合 extends
来约束类型。
function identity6<T extends Length>(arg: T): T {
console.log(arg.length);
return arg;
}
identity6([1, 2]);
identity6('str');
identity6({length: 6});
identity6(6); // error, 类型“number”的参数不能赋给类型“Length”的参数。
2. 检查对象上的键是否存在:
泛型约束还有另一个常见的使用场景,就是检查对象上的键是否存在
,使用 keyof
结合 extends
确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]{
return obj[key]
}
const info = {
name: '影流之主',
age: 18,
desc: '我会影分身'
}
getProperty(info, 'name');
getProperty(info, 'sex'); // error, 类型“"sex"”的参数不能赋给类型“"age" | "name" | "desc"”的参数。
typeof
的主要用途是在类型上下文中获取变量或者属性的类型
interface Info {
name: string,
age: number
}
const info6: Info = {
name: '影流之主',
age: 18,
}
type Info1 = typeof info6; // type Info1 = Info;
const info1: Info = {
name: '凯隐',
age: 16,
}
keyof
可以用于获取某种类型的所有键,其返回类型是联合类型,配合extends
就可以实现泛型约束,就是上面用到的检测对象上的键是否存在的用法。
interface Student1 {
name: string,
id: number
}
type Property = keyof Student1; // 'name' | 'id'
type Property1 = keyof Student1[] // 'length' | 'push' | 'pop' | 'concat' ...
function getProperty<T, K extends keyof T>(arr: T, key: K): T[K]{
return arr[key];
}
const arrPush = getProperty([1, 2, 3], 'push');
总结: 其实就是和for in
遍历对象类似,把这个类型的上所有 property
都放在一个联合类型上。
另外在复习一下for of
和 for in
:
for of
不能遍历不可迭代的对象;比如普通定义的对象就不能遍历,可迭代对象包括: Array
,Map
,Set
,String
,TypedArray
,arguments
等等;for in
可以遍历数组和对象;for of
遍历获取到的是 value
,for in
遍历获取到的是 key
。in
用来遍历枚举类型:
type Keys = 'name' | 'skill' | 'desc';
type Obj = {
[key in Keys]: string
}// {name: string, skill: string, desc: string}
在条件类型语句中,可以用 infer
声明一个类型变量并且对它进行使用。returnType
就是通过 infer
实现的。
returnType
的实现:
type ReturnTypeCustom<T extends (...arg: any[]) => any>
= T extends (...arg: any[]) => infer R ? R : any;
刚看到这一串代码,是不是很懵逼,我们先看一个简单的例子:
// 当我们想获取一个数组或元组里的类型时
type PickType<T> = T extends Names ? 'string' : T extends Ids ? 'number' : T;
type Ids = number[];
type Names = string[];
type Flags = boolean[];
type RandomTuple = [string, number, boolean];
type idType = PickType<Ids>; // 类型推断为 number
type nameType = PickType<Names>; // 类型推断为 string
type flagType = PickType<Flags>; // 类型推断为 boolean[];
// 使用 infer 之后
type PickType1<T> = T extends Array<infer R> ? R : T
type flagType1 = PickType1<Flags>; // 类型推断为 boolean
type randomType = PickType1<RandomTuple>; // 类型推断为 string | number | boolean
上面的 PickType
只能对 string
或者 number
的数组进行类型提取,当遇到 boolean
类型的数组,就是直接返回泛型 T
,也就是 boolean[]
,使用 infer
之后,相当于临时定义了一个 R
,当 R
存在的时候就返回 R
的类型回去。这样再回去看 ReturnType
的实现,就容易理解多了。
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
extends 的使用已经在上方 4.2.5 确保属性存在的例子中展现了 extends 的作用,这里就不做多讲解了。
Partial
的作用就是将 T
里所有的属性全部变为可选项 ?
。
// Partial 部分的;不完全的;
// node_modules\typescript\lib\lib.es5.d.ts
// 官方定义:
type Partial1<T> = {
[K in keyof T]?: T[K]
}
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值的类。中间的 ?
号,用于将所有属性变为可选。
Partial
用法:
interface PersonInfo {
name: string,
age: number,
}
type PersonInfo1 = Partial<PersonInfo>;
// type PersonInfo1 = {
// name?: string | undefined;
// age?: number | undefined;
// }
Required
将T
类型中所有选项变为必选,去除所有?
。和 Partial 刚好相反。
// node_modules\typescript\lib\lib.es5.d.ts3
// 官方定义:
type Required<T> = {
[P in keyof T]-?: T[P];
};
上面这串代码应该就是-?
这个字符有点不太理解,把 -
当做运算符来看就好理解了,-?
相当于就是把这个属性上的 ?
移除。
Readonly
将T
类型中所有选项变为readonly
。
// node_modules\typescript\lib\lib.es5.d.ts3
// 官方定义:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Record
的作用是将 K
中所有的属性的值转化为 T
类型。
// node_modules\typescript\lib\lib.es5.d.ts3
// 官方定义:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
BTW:K extends keyof any
, keyof any
是 string | number | symbol
,对象中的 key
只能是这三种。
Record
用法:
// Record
interface PageInfo {
title: string;
}
type page = 'home' | 'about' | 'contact';
const p: Record<page, PageInfo> = {
home: {title: 'homeTitle'},
about: {title: 'aboutTitle'},
contact: {title: 'contactTitle'},
}
// p 的类型得是
// {
// home: {title: string},
// about: {title: string},
// contact: {title: string},
// }
Pick
从T
中挑出 K
属性。
// node_modules\typescript\lib\lib.es5.d.ts
// 官方定义:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Pick
用法:
interface PersonInfo {
name: string,
age: number,
}
const personInfo1: Pick<PersonInfo, 'name'> = {
name: '劫'
}
// personInfo1 类型就是 {name: string}
type PersonProperty = 'name' | 'age';
const personInfo2: Pick<PageInfo, PersonProperty> = {
name: '小鱼',
age: 8
}
//personInfo2 类型就是 {name: stirng, age: number}
ExcludeU
类型中的 T
属性给移除掉。
// node_modules\typescript\lib\lib.es5.d.ts
// 官方定义:
type Exclude<T, U> = T extends U ? never : T;
Exclude
用法:
type ArrayMethod = 'push' | 'pop' | 'concat' | 'shift' | 'unshift';
type ExcludePushAndPop = Exclude<ArrayMethod, 'push' | 'pop'>;
// "concat" | "shift" | "unshift"
OmitT
类型中不包含 K
的所以属性给移除掉,并且返回一个新的类型。
// node_modules\typescript\lib\lib.es5.d.ts
// 官方定义:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Omit
用法:
type ArrayMethod = 'push' | 'pop' | 'concat' | 'shift' | 'unshift';
type ExcludePushAndPop1 = Omit<ArrayMethod, 'push' | 'pop'>;
这么一看你是不是觉得和 Exclude
是一样的呢?那你就错了,可以看到 Omit
的实现 Exclude
的时候是对 T类型进行了一个 keyof
操作,这样这个类型上所有的属性都会遍历一遍,你可能忽略了类型身上的一些方法,也是属于它身上的熟悉,所以 ExcludePushAndPop1
最终的类型是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-11S1ezVt-1659687092630)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb06bbb0c92647979ef344b8c5df054e~tplv-k3u1fbpfcp-watermark.image?)]
typescript 官网
1.2W字 | 了不起的 TypeScript 入门教程
2021 typescript史上最强学习入门文章(2w字)
一文读懂 TypeScript 泛型及应用( 7.8K字)
轻松拿下 TS 泛型
深入理解 TypeScript