由于前团队习惯(重中之重:个人未坚持使用)等一些不是原因的原因,ts 未很好地用起来,使用停留在上一个产品中,还是因为中后台数据场景使用了 antd-pro 的组件,而默认示例是 ts,所以简简单单用了一下。碍于本身项目不大,也不是有很多需要设计的地方,因此感觉并未真正入门。
当下刚好有票圈大佬组织学习,就迫不及待加入了,弹弹《Typescript 全面进阶指南》的灰,好文要早点吸收掉。本篇笔记更多的是为了给自己看的,先记一波当天,结合前边笔记再串一下,最终会整合成一篇相对容易理解的文章。
磨刀不误开柴工
VS Code 配置与插件
-
VS Code settings - typescript 配置哪些推导类型需要直接显示在工作区域(默认需要 hover 才会显示类型)
-
ErrorLens 错误显示在工作区域
TS 文件的快速执行:ts-node 与 ts-node-dev
-
ts-node 快速执行 ts 文件
-
ts-node-dev 直接执行 ts 文件,并且支持监听文件重新执行。
基于 node-dev(可以理解一个类似 nodemon 的库,提供监听文件重新执行的能力) 与 ts-node 实现,并在重启文件进程时共享同一个 TS 编译进程,避免了每次重启时需要重新实例化编译进程等操作。
类型检查
简单类型介绍
基础类型
js 中对应 8 种数据类型都一一对应,除了 null 与 undefined 有特别的用法。
const name: string = 'linbudu';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');
null 与 undefined
TypeScript 中,null 与 undefined 类型都是有具体意义的类型。也就是说,它们作为类型时,表示的是一个有意义的具体类型值。
===> 🚩这两者在没有开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 null 与 undefined 类型:
const tmp1: null = null;
const tmp2: undefined = undefined;
const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立,下同
const tmp4: string = undefined;
void
void 表示一个空类型,undefined 能够被赋值给 void 类型的变量,就像在 JavaScript 中一个没有返回值的函数会默认返回一个 undefined。
===> 🚩 null 类型也可以,但需要在关闭 strictNullChecks 配置的情况下才能成立。
const voidVar1: void = undefined;
const voidVar2: void = null; // 需要关闭 strictNullChecks
declare
只是想要进行类型比较
interface Foo { name: string; age: number; }
interface Bar { name: string; job: string; }
declare let foo: Foo;
declare let bar: Bar;
foo = bar;
数组
// 基础用法1
const arr1: string[] = [];
// 基础用法2
const arr2: Array<string> = [];
// 不可修改整个数组 readonly
const mixedArray: readonly (string | number)[] = ["apple", 1, "cherry", 2];
mixedArray[0] = 'lll'; // ❌ Index signature in type 'readonly (string | number)[]' only permits reading
// 不可修改整个数组 as const
const fruits = ["apple", "banana", "cherry", { name: 'kkkk'}] as const;
fruits[0] = 'kkk'; // ❌ Cannot assign to '0' because it is a read-only property
fruits[3].name = 'kkk'; // ❌ Cannot assign to 'name' because it is a read-only property
元组
const arr6: [string, number?, boolean?] = ['linbudu'];
type TupleLength = typeof arr6.length; // 1 | 2 | 3
// 具名元组
const arr7: [name: string, age: number, male?: boolean] = ['linbudu', 599, true];
// 防止显示越界访问
const arr5: [string, number, boolean] = ['linbudu', 599, true];
console.log(arr5[5]); // ❌ Tuple type '[name: string, desc: string, age: number]' of length '3' has no element at index '5'.
// 防止隐式越界访问
const [name, age, male, other] = arr5; // ❌ Tuple type '[name: string, desc: string, age: number]' of length '3' has no element at index '3'
对象
一般 interface 能表达的类型 type 都可以表示,建议对象优先 interface,type 来表示联合类型、一个工具类型等等抽离成一个完整独立的类型。
- Optional 模式下虽然 func 赋值了,但我们定义的类型是 Function | undefined
- Readonly 模式与数组不同的是,后者是对整个数组 Readonly,而对象可以对属性 Readonly,
// 可选 Optional
interface IDescription { name: string; age: number; male?: boolean; func?: Function; }
const obj1: IDescription = { name: 'linbudu', age: 599, male: true, func: () => {} };
obj1.func(); // ❌ Cannot invoke an object which is possibly 'undefined'
// 只读(Readonly)
interface IDescription { readonly name: string; age: number; }
const obj3: IDescription = { name: 'linbudu', age: 599, };
// 无法分配到 "name" ,因为它是只读属性
obj3.name = "林不渡";
类型实现只有声明的部分而报错怎么办?使用断言:
interface IStruct {
foo: string;
bar: () => Promise<void>;
}
const temp8: IStruct = {
foo: ''
} // ❌ Property 'bar' is missing in type '{ foo: string; }' but required in type 'IStruct'.
// 断言
const temp7 = {
bar: handler: () => Promise.resolve()
} as IStruct;
object、Object 以及 { }
🌟 在 object、Object 以及 { } 中,undefined、null、void都可以作为子类型,但 这三个都需要在 strictNullChecks 模式下才会成立所属关系!
Object 一切类型
❗️在任何情况下,都不应该使用这些装箱类型。
和 Object 类似的还有 Boolean、Number、String、Symbol,这几个装箱类型(Boxed Types) 同样包含了一些超出预期的类型。以 String 为例,它同样包括 undefined、null、void (这三个需要在 strictNullChecks 模式下才会成立所属关系),以及代表的 拆箱类型(Unboxed Types) string。
strictNullChecks:
- 类型:
boolean - 默认值:
false - 作用:这个选项用于启用或禁用 TypeScript 的严格空检查(Strict Null Checks)。当启用时(
strictNullChecks: true),TypeScript 将对可能为null或undefined的值进行更严格的类型检查,以避免潜在的空引用错误。这有助于提高代码的可靠性和安全性。
// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;
object
object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型这些。
{}
{}作为类型签名就是一个合法的,但内部无属性定义的空对象。与 Object 对比, {} 无法进行任何赋值操作。
字面量类型
字面量类型主要包括字符串字面量类型、数字字面量类型、布尔字面量类型和对象字面量类型,它们可以直接作为类型标注。无论是原始类型还是对象类型的字面量类型,它们的本质都是类型而不是值。它们在编译时同样会被擦除,同时也是被存储在内存中的类型空间而非值空间
为什么要有字面量类型?原始类型的值可以包括任意的同类型值,而字面量类型要求的是值级别的字面量一致。场景:从宽泛的原始类型直接收窄到精确的字面量类型集合。
字面量一般不会单独存在,通常在联合类型中使用:
type nums = 1 | 2 | 3;
联合类型
联合类型不仅可以像上边所说的组合字面量,它是可以组装任何类型的类型合集。
interface Tmp { mixed: true | string | 599 | {} | (() => {}) | (1 | 2) }
📌 函数类型是一种类型而不是具体的函数,并且要用括号括起来。
枚举
// ts
enum Color {'red' = 0, 'green'};
// 编译后的 js 代码侵入,📌 TODO: 目前发现唯一侵入的 api
var Color;
(function (Color) {
Color[Color["red"] = 0] = "red";
Color[Color["green"] = 1] = "green";
})(Color || (Color = {}));
unique symbol
在 TypeScript 中,symbol 类型并不具有独一无二的特性,只能表示同为 symbol 类型,不能表示要相同的值。因此 unique symbol 就是为了解决这个问题 - 维护了 Symbol('string') 的独一无二特性。
const uniqueSymbolFoo: unique symbol = Symbol('kkk');
const uniqueSymbolBaz: typeof uniqueSymbolFoo = uniqueSymbolFoo;
any Top Type
类型检查无法束缚的类型,可以在任意时刻代表所有类型。
unknow Top Type
与 any 的区别:unknow 代表当下不确定类型,但在未来某一刻会确定类型的类型定义,无法将 unknow 类型的值赋值给除 any 和 unknow 之外类型的变量。
never Bottom Type
never 类型不携带任何的类型信息,是整个类型系统层级中最底层的类型。和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。如果不能理解,感受下使用场景:
// 报错的函数永远都无法执行到正确返回
function justThrow(): never {
throw new Error()
}
declare const strOrNumOrBool: string | number;
if (typeof strOrNumOrBool === "string") {
// 一定是字符串!
strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
strOrNumOrBool.toFixed();
} else {
// 如果一次更新中,扩展了 strOrNumOrBool 的类型,加了 Boolean,但是条件判断中忘记加了,此时赋值给 `never` 类型的 _exhaustiveCheck 就有 ts 检查报错,辅助开发
const _exhaustiveCheck: never = strOrNumOrBool;
throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}
断言
// 双重断言 当类型无交叉时,也就是太离谱毫无瓜葛时,断言为两者公共的 top type 类型作为缓冲区
const str: string = "linbudu";
(str as unknown as { handler: () => {} }).handler();
// 非空断言
foo.func!().prop!.toFixed();
// 否则
((foo.func as () => ({
prop?: number;
}))().prop as number).toFixed();
类型工具
类型别名
方便类型复用,或生成强化类型
type StatusCode = 200 | 301 | 400 | 500 | 502;
const status: StatusCode = 502;
泛型
类型界函数
type Factory<T> = T | number | string;
const foo: Factory<boolean> = true;
- 分布式条件类型中
any和never的特性:当条件类型的判断参数为 any,会直接返回条件类型两个结果的联合类型。而在这里其实类似,当通过泛型传入的参数为 never,则会直接返回 never
infer
只能放在
条件语句中
// 递归
type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T;
索引类型
以下几点均为了理解该综合使用:映射 + 索引类型查询 + 索引访问
type Clone<T> = { [K in keyof T]: T[K]; };
索引签名类型
由于 JavaScript 中,对于 obj[prop] 形式的访问会将数字索引访问转换为字符串索引访问,也就是说, obj[599] 和 obj['599'] 的效果是一致的。因此,在字符串索引签名类型中我们仍然可以声明数字类型的键。类似的,symbol 类型也是如此:
interface AllStringTypes {
[key: string]: string;
}
const foo: AllStringTypes = {
"linbudu": "599",
599: "linbudu",
[Symbol("ddd")]: 'symbol',
}
// 索引签名类型也可以和具体的键值对类型声明并存,但必须符合索引签名类型的声明:
interface AllStringTypes {
// 类型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
propA: number;
[key: string]: boolean;
}
// 在重构 JavaScript 代码时,为内部属性较多的对象声明一个 any 的索引签名类型,以此来暂时支持**对类型未明确属性的访问**,并在后续一点点补全类型
interface AnyTypeHere { [key: string]: any; }
索引类型查询
keyof
产物必定是一个联合类型,比如 keyof any 所有可用作 key 的类型集合: string | number | symbol。
interface Foo {
linbudu: 1,
599: 2
}
type FooKeys = keyof Foo; // "linbudu" | 599
// 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
// 看不到其中的实际值,你可以这么做:
type FooKeys = keyof Foo & {}; // "linbudu" | 599
索引类型访问
interface NumberRecord {
[key: string]: number;
}
type PropType = NumberRecord[string]; // number
interface Foo {
propA: number;
propB: boolean;
}
type PropAType = Foo['propA']; // number 'propA' 表示字面量类型,而不是 js 字符串值
type PropBType = Foo['propB']; // boolean
interface Foo {
propA: number;
propB: boolean;
propC: string;
}
type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean
映射类型
interface IFoo {
[K: symbol]: number;
propsA: boolean;
}
type Stringfy<T> = {
[K in keyof T]: string;
}
type TFoo = Stringfy<IFoo>; // type TFoo = { [x: symbol]: string; propsA:string; }
类型层级
边学习边补充,持续更新中...
any、unknown --> Object --> String 等装箱类型 --> 原始类型、对象类型 --> 字面量类型 --> never
📌 TODO: 联合类型这种集合可能性包含上述整条链路,暂时感觉放哪里都不合适
类型工具
Record<Keys, Type>
Constructs an object type whose property keys are Keys and whose property values are Type. This utility can be used to map the properties of a type to another type.
Example
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
cats.boris;
const cats: Record<CatName, CatInfo>
工具库
tsd 声明式的类型检查
原理篇
TODO
- extends 判断类型之间的兼容性
不太容易理解的点
-
infer
a. ❓❓❓ 泛型参数 V 的来源是从键值类型推导出来的,TypeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失
type ArrayItemType<T> = T extends Array<infer ElementType> ? ElementType : never; // 空值无法推断元素类型,所以是 never,而不是 any type ArrayItemTypeResult1 = ArrayItemType<[]>; // never type ArrayItemTypeResult3 = ArrayItemType<[string, number]>; // string | number `[string, number]` 实际上等价于 `(string | number)[]` // 反转键名与键值 type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never type ReverseKeyValueResult1 = ReverseKeyValue<{ "key": "value" }>; // { "value": "key" } // 类型“V”不满足约束“string | number | symbol”。 // ??? 泛型参数 V 的来源是从键值类型推导出来的,TypeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失 type ReverseKeyValue1<T extends Record<string, string>> = T extends Record< infer K, infer V > ? Record<V, K> : never; -
any
如果交叉类型的其中一个成员是 any,那短板效应就失效了,此时最终类型必然是 any 。
type Tmp4 = 1 & any; // any