likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 61

大家好,我卡颂。

  1. 在这篇文章中记录每节课的笔记

  2. 如果哪个工作日没学,就在小册群发20元红包

看看这样能不能提高我的学习动力,哈哈哈哈

几个好用的TS相关工具:

插件

npm包

  • ts-node,ts的repl。与tsc的关系是:tsc是ts编译器,ts-node是ts执行环境,后者依赖前者

  • ts-node-dev,能监听文件变化并重启的ts-node,基于ts-nodenode-dev实现

一个node知识

require hook:当 Node.js 的模块系统试图加载一个文件时,require 函数是用来导入模块的。通过使用 require hook,开发者可以控制并修改模块的加载过程。用途例如:

  • 编译转换:转换被 require 的文件的内容,这在编译 TypeScript 或 Babel(用于编译 ES6+ 代码)时特别有用。
  • 模拟:在测试中模拟某些模块的行为。
  • 代码检查或修改:在模块加载前对其进行代码质量检查或者动态修改。

对于如下代码:

node -r ts-node/register index.ts
  • -r 是 --require 的简写,该参数后面跟随的模块将在输入的脚本执行前预加载
  • ts-node/register 是require hook,由ts-node 提供
  • index.ts 是要执行的 ts 文件

Day 2

当未开启strictNullChecks时,undefinednull可以作为其他类型的子类型。

void表示一个空类型,而nullundefined是具有意义的实际类型。

type与interface的取舍

type(Type Alias,类型别名)职责:将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型。

interface职责:用来描述对象、类的结构

但大部分场景下接口结构都可以被类型别名所取代,因此,只要你觉得统一使用类型别名让你觉得更整齐,也没什么问题。

3个对象相关类型

object代表所有非原始类型的类型,即数组、对象与函数类型这些

const tmp22: object = { name: 'linbudu' }; 
const tmp23: object = () => {}; 
const tmp24: object = [];

Object是装箱类型,原型链的顶端是 Object 以及 Function,这也就意味着所有的原始类型与对象类型最终都指向 Object,在 TypeScript 中就表现为 Object 包含了所有的类型。但不应该使用它

{}代表对象字面量类型,或者叫***内部无属性定义的空对象,可以表示任何非 null / undefined 的值,不应该使用它。

const tmp28: {} = 'linbudu'; 
const tmp29: {} = 599; 
const tmp30: {} = { name: 'linbudu' };
const tmp31: {} = () => {}; 
const tmp32: {} = [];

同时,也不能给他赋值。

Day 3

字面量类型包括numberbooleanobjectstring的字面量:

declare var a: 100
declare var b: true
declare var c: 'hello'
declare var d: {}

字面量类型是对应基础类型的子类型。

联合类型的常用场景

联合类型的常用场景之一是通过多个对象类型的联合,来实现手动的互斥属性,即这一属性如果有字段1,那就没有字段2:

interface Tmp {
    user: {
        vip: true
        expires: string
    } | {
        vip: false
        promotion: string
    }
}

declare var t: Tmp
if (t.user.vip) {
    console.log(t.user.expires)
}

当然,上面的代码运行时肯定会报错啦~~

枚举

定义一组命名的常量(非常量枚举可以定义延迟计算的枚举值),作用包括:

  1. 类型安全

可以防止传入无效的值

enum Status {
  PENDING,
  IN_PROGRESS,
  DONE,
  CANCELLED
}

function updateTaskStatus(taskId: number, status: Status): void {
  // 函数内部逻辑...
}

updateTaskStatus(1, Status.DONE); // 正确
updateTaskStatus(2, 3); // 正确,因为数字也被认为是有效的枚举成员的值
updateTaskStatus(3, "DONE"); // 错误:Type '"DONE"' is not assignable to type 'Status'.
updateTaskStatus(4, 5); // 错误:Argument of type '5' is not assignable to parameter of type 'Status'.
  1. 自文档化

提高代码可读性

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}

function move(characterId: number, direction: Direction): void {
  // 根据方向移动角色...
}

move(1, Direction.LEFT); // 代码清晰表明意图,而不是使用一个难以理解的数字或字符串
  1. 重构友好

需求变更后,只需调整枚举值,不需调整枚举成员

enum Color {
  RED = "#FF0000",
  GREEN = "#00FF00",
  BLUE = "#0000FF"
}

// 假设我们之后需要将 RED 的代码从 "#FF0000" 改为 "#FF0101"
enum Color {
  RED = "#FF0101", // 只需在这里改动
  GREEN = "#00FF00",
  BLUE = "#0000FF"
}

function setColor(elementId: number, color: Color): void {
  // 设置元素颜色...
}

setColor(1, Color.RED); // 不需要修改使用了 Color.RED 的任何地方

默认情况下,枚举值是数字

enum Items { Foo, Bar, Baz }
// Items.Foo === 0
// Items.Bar === 1

数字枚举值可以双向映射(键 -> 值 and 值 -> 键),对象只能单向映射(键 -> 值)

enum Items { Foo, Bar, Baz }

Items.Foo === 0 // 0是 value
Items[0] === 'Foo' // 'Foo'是 key

对应的编译结果:

"use strict";
var Items;
(function (Items) {
    Items[Items["Foo"] = 0] = "Foo";
    Items[Items["Bar"] = 1] = "Bar";
    Items[Items["Baz"] = 2] = "Baz";
})(Items || (Items = {}));

数字枚举值的双向映射的好处:

  • 调试友好,从值可以反查键
  • 方便,比如存库的时候根据值保存键,select options中显示键,value为值

Day 4

函数

函数类型相关包括三部分:

  • 参数
  • 逻辑
  • 返回值

当聊到函数类型,主要讲的是参数和返回值类型,比如函数重载

函数重载的作用:将入参与返回值类型关联,包括两部分:

  • 重载签名

  • 实现签名

// 前两个是重载签名
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;

// 实现签名
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

但是,ts type checker只检查重载签名实现签名之间的兼容性,而不会对具体逻辑进行详尽分析,以确保每个分支严格按照重载签名的类型返回,比如:

function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    // 这里应该只能返回number,但返回string也不会报错
    return '123123';
  }
}

ts中重载类似伪重载,与其他语言重载的区别:

  • ts重载体现在方法调用的签名上,而非实际实现上
  • 一些语言(如C++)重载体现在多个名称一致,但入参不同的函数的实现上

Class

Class相关类型相关的包括:

  • 构造函数
  • 属性
  • 方法
  • 访问符

类修饰符

包括:

  • pubic,访问性修饰符,此类成员在类、类的实例、子类中都能被访问
  • private,访问性修饰符,此类成员仅能在类的内部被访问
  • protected,访问性修饰符,此类成员仅能在类与子类中被访问
  • readonly,操作性修饰符

一个语法糖 —— 对构造函数参数使用访问性修饰符,可以直接创建对应的实例属性,而不需要在构造函数中赋值(this.xxx = xxx)

class Foo {
  constructor(public arg1: string, private arg2: boolean) { }
}

const f = new Foo("linbudu", true)
console.log(f.arg1) // "linbudu"
console.log(f.arg2) // 报错,这是private

关键字

包括:

  • static,类的静态属性、方法
  • override,用于指明派生类中覆盖基类的方法,如果基类中不存在对应方法会报错
class Foo {}

class FF extends Foo {
    // 报错
    override hello() {}
}

Class的类型

包括:

  • 基类(class XX {})
  • 派生类(class YY extendx XX {})
  • 抽象类(abstract class ZZ {}),描述了一个类中应当有哪些成员(属性、方法等)
  • 抽象类的派生类(class ZZZ implements ZZ {})
abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

抽象类需要实现(implements):

class Foo implements AbsFoo {}

抽象类与基类的区别:

  • 抽象类不能被实例化,
  • 可以申明抽象成员,这些成员必须在派生类中实现
  • 抽象类适合作为其他类的公共基础架构

当前遇到的几种interface

包括:

  • 普通对象的interface
  • 对于函数的 callable interface
interface A {
    (name: string): number
}
  • 对于类的 newable interface
class Foo {}
interface Fooo {
    new(): Foo
}
declare const NewableFoo: Fooo;
const foo = new NewableFoo()

Day 5

Top type与Bottom type

anyunknown都是Top type,包含其他所有类型。比如断言时差异较大(没有共同的父类型)的类型可以先断言到top type,在断言回来:

declare const a: boolean;
(a as string) // 报错
(a as unknown as string).length

neverbottom type,是所有类型的子类型。

整个type体系大体可以看作:

  • top typeanyunknown
  • 特殊的Object,它也包含了所有的类型(因为原型链),但和Top Type比还是差了一层
  • StringBooleanNumber这些装箱类型
  • 原始类型与对象类型
  • 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是nullundefined并不是字面量类型的子类型
  • 最底层的never

unknown、never的区别

unknownany一样是top type,用于类型后续才能确定的场景,像any一样类型为unknown的变量可以赋值为其他类型:

let a: unknown;

a = 1;
a = 'a'
a = () => {}

unknown类型的变量只能赋值给anyunknown类型的变量:

const v1: string = a; // 报错 Type 'unknown' is not assignable to type 'string'.
const v2: any = a;
const v3: unknown = a;

never代表永远无法到达的代码、不存在的状态

function a():never {
    throw 'xxx';
    // 永远无法有返回值
}

never是所有类型的子类型,所以never可以赋值给void,但void表示没有任何值,是个具体的概念,所以不能赋值给never

declare let v1: never;  
declare let v2: void;  
v1 = v2; // Type 'void' is not assignable to type 'never'.
v2 = v1;

Day 6

类型的按位操作

按位或(|):联合类型,取两种类型的集合

按位与(&):交叉类型,取两种类型的交集

交叉类型与top type的合用,其中:

  • {}unknowntop type,与KA的交集应该是KA
  • any虽然是top type,但any会忽略类型检查,所以变成了any
interface A {
    name: string
    2: number
}
type KA = keyof A // keyof A
type B = KA & {} // 'name' | 2
type C = KA & unknown // keyof A
type D = KA & any // any
type E = KA & void // never

索引签名类型

形如如下的形式:

interface A {
    [key: string]: string
}

索引类型查询

keyof操作符,返回索引所有key对应类型字面量的联合类型,如:

interface A {
    name: string;
    123: 321;
    age: 33
}
keyof A // 'name' | 123 | 'age'

映射类型

type Stringify<T> = {
  [K in keyof T]: string;
};

[K in keyof T]: string;是一个映射类型(Mapped Type)的语法。这个特定的映射类型 Stringify<T> 会将一个类型 T 的所有属性的类型转换成 string 类型

使用:

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 等价于
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}

Day 7

几个类型守卫相关概念:

  • is,当类型守卫函数返回trueis前的传参可以被认为是某种类型,这个信息会给到类型守卫函数的调用方,用于接下来的类型控制流分析
// 类型守卫
function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 正确了
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}
  • in
function handle(input: Foo | Bar) {
  if ('shared' in input) {
    // 类型“Foo | Bar”上不存在属性“fooOnly”。类型“Bar”上不存在属性“fooOnly”。
    input.fooOnly;
  } else {
    // 类型“never”上不存在属性“barOnly”。
    input.barOnly;
  }
}
  • typeof,用于基本类型

  • instanceof,用于引用类型,包括判断A是否是B的实例

class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}
class Bar extends BarBase {
  barOnly() {}
}

function handle(input: Foo | Bar) {
  if (input instanceof FooBase) {
    input.fooOnly();
  } else {
    input.barOnly();
  }
}
  • asserts,类型断言守卫,有点类似is,当类型断言守卫函数没有报错时,asserts前的传参可以被认为是某种参数。可以和is合用
function assert(condition: any, msg?: string): asserts condition {
  if (!condition) {
    throw new Error(msg);
  }
}


let name: any = 'linbudu';

function assertIsNumber(val: any): asserts val is number {
  if (typeof val !== 'number') {
    throw new Error('Not a number!');
  }
}

assertIsNumber(name);

// number 类型!
name.toFixed();

Day 8

泛型的本质:基于调用时类型推导自动填充参数的类型,从而让多个位置(函数内部、返回值)的类型存在约束或关联,从而实现更严格的类型保护。

type Partial<T> = { [P in keyof T]?: T[P]; };

extends 在不同语境下的含义

  1. 类继承 子类
class Dog extends Animal {
  bark() {
    console.log('Woof! Woof!');
  }
}
  1. 接口继承 扩展接口
interface A extends B {}
  1. 泛型约束 使用泛型类型参数时,extends 可以约束一个泛型参数必须是某个特定类型的子类型。
function logIdentity<T extends { toString(): string }>(arg: T): T {
  console.log(arg.toString());
  return arg;
}
  1. 条件类型

判定类型兼容性

type IsNumber<T> = T extends number ? "Yes" : "No";

Day 9

区分两个概念:

  • 类型:值所属的集合,以及值上可执行操作的描述
  • 类型系统:一套关于类型的规则(哪些类型是有效的,类型之间如何相互作用、如何兼容)

TS中类型系统的特点:

  • 是结构化系统(structual system),不是指称系统(nominal system)
  • 是编译时检查,而不是运行时检查(js是运行时检查类型)

其中:

  • 结构化系统判断两个类型是否为同类时,只要两者有相同方法(返回值类型也一致)、相同属性即可。判断父子关系只需要后者与前者有相同方法、属性。即鸭子类型
  • 指称系统判断两个类型为同类,需要两者名字相同。判断父子关系需要严格的继承
class Cat {
  eat() { }
}

class Dog {
  eat() { }
}

function feedCat(cat: Cat) { }

feedCat(new Dog()) // 不报错

结构化系统模仿指称系统:

declare class TagProtector<T extends string> {
  // 用来携带额外信息,使结构化系统能区分
 protected __tag__: T;
}
type Nominal<T, U extends string> = T & TagProtector<U>;


// 使用
type CNY = Nominal<number, 'CNY'>;
type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
 return (source + input) as CNY;
}

addCNY(CNYCount, CNYCount);

// 报错了!
addCNY(CNYCount, USDCount);

Day 10

Day 11

infer

infer关键字用于提取条件类型中提取类型,下面infer RR指代函数返回值的类型:

type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

返回元组首尾类型交换后的元组:

type Swap<T extends any[]> = T extends [infer x, ...infer y, infer z] ? [z, ...y, x] : T;

提取Promise Resolve类型(支持嵌套promise):

type PromiseResolveType<T> = T extends Promise<infer r> ? (r extends Promise<any> ? PromiseResolveType<r> : r) : never;

分散条件类型

条件语句中,如果通过泛型传入联合类型,且条件语句判断的是裸泛型类型,则判断时会将传入的泛型联合类型分别去比较,再返回所有比较结果的联合类型。 下例中:

  • 传给Res1的泛型(1 | 2 | 3 | 4 | 5)在Condition内部是裸泛型,触发分散条件类型
  • Res2没有通过泛型传入,即使是条件语句,也不会触发分散条件类型
type Condition<T> = T extends 1 | 2 | 3 ? T : never;

// 1 | 2 | 3
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;

// never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;

下例中,Wrapped中参与比较的不是裸泛型(包裹在元组中),所以不会触发分练条件类型:

type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Res3 = Naked<number | boolean>;

// "N"
type Res4 = Wrapped<number | boolean>;

Day 11

一些内置结构工具的简单实现:

Record<K, V>:返回一个key类型为KValue类型为Vinterface

// K属于所有键名联合类型的子集
type Record1<K extends keyof any, V> = {
    [key in K]: V
}

Pick<Obj, Key>:返回interface Obj中包含键名联合类型Keyinterface

type Pick1<Obj, K extends keyof Obj> = {
    [Key in K]: Obj[Key]
}

Omit<Obj, Key>:从interface Obj中移除包含键名联合类型Key后,返回剩下键组成的interface

// 利用分散条件类型,实现的差集
type Exclude1<A, B> = B extends A ? never : B;

type Omit1<Obj, Key> = Pick<Obj, Exclude1<keyof Obj, Key>>;
转载自:https://juejin.cn/post/7311279319416979496
评论
请登录