学习笔记 - 《Typescript 全面进阶指南》

196 阅读11分钟

由于前团队习惯(重中之重:个人未坚持使用)等一些不是原因的原因,ts 未很好地用起来,使用停留在上一个产品中,还是因为中后台数据场景使用了 antd-pro 的组件,而默认示例是 ts,所以简简单单用了一下。碍于本身项目不大,也不是有很多需要设计的地方,因此感觉并未真正入门。

当下刚好有票圈大佬组织学习,就迫不及待加入了,弹弹《Typescript 全面进阶指南》的灰,好文要早点吸收掉。本篇笔记更多的是为了给自己看的,先记一波当天,结合前边笔记再串一下,最终会整合成一篇相对容易理解的文章。

磨刀不误开柴工

VS Code 配置与插件
  • TypeScript Importer

  • Move TS

  • 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 将对可能为 nullundefined 的值进行更严格的类型检查,以避免潜在的空引用错误。这有助于提高代码的可靠性和安全性。
// 对于 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 类型的值赋值给除 anyunknow 之外类型的变量。

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;
  • 分布式条件类型中 anynever 的特性:当条件类型的判断参数为 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 判断类型之间的兼容性

不太容易理解的点

  1. 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;
    
  2. any

    如果交叉类型的其中一个成员是 any,那短板效应就失效了,此时最终类型必然是 any 。

    type Tmp4 = 1 & any; // any