关于前端:深入学习typescript-type类型

41次阅读

共计 4925 个字符,预计需要花费 13 分钟才能阅读完成。

当个题目党,其实并不算太深刻

所在仓库地址:https://github.com/goblin-pitcher/steel-wheel-run/blob/master/typescript/type-challenge-pre-content.md

目标

最近筹备开始刷type-challenges,因而先梳理一下 ts 类型相干知识点。

脱漏知识点总结

ts的 type 合乎图灵齐备 。这意味着ts 类型蕴含循环、判断等一系列基本操作。

类型汇合

类型该当作汇合来看,其中:

  1. unknown是选集,蕴含所有类型,是所有类型的父级

    • 之前在泛型中会写 Comp<T extends unknown> 看来是有些多此一举了 …
  2. never是空集,是所有类型的子集

ts 的设计合乎 里氏替换准则,即子集能够替换所有父级,这意味着:

interface Parent {id: string;}
// 合乎里氏替换准则,不会报错
interface Child extends Parent {
  id: "childIdA"|"childIdB"; // "childIdA"|"childIdB" 是 string 的子集
  key: number; // Parent 中没有 key,该属性是 Parent 的拓展
}
// 不合乎里氏替换准则,报错
interface ChildError extends Parent {id: number; // number 不是 string 的子集}


type A = {a: string};
type B = {a: "A"|"a", b: number};
let a: A = {a: "xxx"};
let b: B = {a: "a", b: 2};
// 合乎 B 是 A 的子集,该赋值合乎里氏替换准则, 反之 b=a 会报错
a = b;

将 TS 的 type 看作编程语言

既然 ts 的 type 是图灵齐备的,那么它天然能够实现所有计算,因而能够看作是一门语言

函数

在类型语境中,可将泛型看做是函数

type Fn<T> = T; // 看作 const Fn = val => val;
循环

常常能够在代码中看到如下写法:

type R = "A" | "B"
type T = {[k in R]: k
}

这里 [k in R] 可看做循环for(const k in R){}

借助此个性,能够实现如下操作:

type MyPick<T, K extends keyof T> = {[k in K]: T[k]
}

type A = MyPick<{a: string, b: number, c: boolean}, "a"|"c">

当然这种循环只针对特定类型("a"|"b"这种),略微简单点的循环还是得通过 递归 实现,例子的话,前面用 ts type 实现四则运算时会用到。

条件语句

type反对三元运算符,这个没什么好说的。。

须要留神的是,type里没有等号,但能够用 extends 代替等号

type Equal5<T> = T extends 5 ? true : false;

来点练习

// eq1: 实现 `GetProp<Obj, K>`(获取 Obj[K]类型)type GetProp<Obj, K> = K extends keyof Obj ? Obj[K] : undefined;
// eq2: 实现 getName<User>
type GetName<User> = GetProp<User, "name">

extends也常和 infer 一起用于类型推断。

例如

// 实现 KeyOf
type KeyOf<T> = T extends {[k in infer U]: unknown} ? U : never;
// 实现 ValueOf
type ValueOf<T> = T extends {[k in keyof T]: infer U } ? U : never;

// 实现 Parameters 和 ReturnType
type FuncBase<F, C extends "params"|"return"> = F extends (...params: infer P) => infer R ? (C extends "params" ? P : R) : never;

type Params<F> = FuncBase<F, "params">;
type Return<F> = FuncBase<F, "return">;
赋值

赋值局部参看后面 类型汇合 章节,赋值要遵循里氏替换准则。

条件语句 章节提到了 infer,或者能够用infer 实现解构赋值。

// 须要实现相似 js 的 const {name, ...extra} = user, 求 extra。(其实就是 Omit 办法)type MyPick<Obj, T extends keyof Obj> = {[k in T]: Obj[k]};
type MyExclude<Obj, T extends keyof Obj> = keyof Obj extends T|(infer U extends keyof Obj) ? U : never;
type MyOmit<Obj, T extends keyof Obj> =  MyPick<Obj, MyExclude<Obj, T>>;

// 实现数组的解构赋值 const [A, ...extra] = arr;
type GetArrBase<T extends unknown[], C extends "first"|"rest"> = T extends [infer First, ...infer Rest] ? (C extends "first"?First: Rest): never;

type GetFirst<T extends unknown[]> = GetArrBase<T, "first">;
type GetRest<T extends unknown[]> = GetArrBase<T, "rest">;
对象

type的对象能够类比 js 中的对象,应用办法如下,留神最初一个例子

type Obj = {
  name: string;
  age: 20;
}

Obj["name"] // string;
Obj.age // Error
Obj["age"] // 20
Obj['name'|'age'] // string | 20 , 这个个性很重要!!!

利用这个个性,能够实现如下性能

interface Test {
  a: string;
  b: number;
  c: boolean;
}
// 之前不晓得这个个性时,用 infer 也能达到同样的成果,但实现不如这个直观
type ValueOf<T> = T[keyof T];
type R = ValueOf<Test>;
数组

ts中的数组分为 Array 数组和 Tuple 元组。

Array数组是诸如 string[] 的写法,相似 java 或其余语言的数组。

Tuple元组更像是 js 中的数组,写法是 [string, number, boolean] 这种。

(注:js中不存在真正意义上的数组,数组是在内存上开拓间断空间,每个单元格所占内存都一样,在 js 中,数组写成 ['a', 5555, {a: 1}] 都没问题,显然在实现上不是真正的开拓了间断内存空间,应该是用链表模仿的,为了解决链表自身查问慢的问题,应该是采纳了跳表或者红黑树的形式组织的?)

数组中须要留神的点如下:

type A = string[];
type B = [string, number, boolean];
// ========================= 分割线 ==========================
// 重点留神!!!A[0]; // string
A[1]; // string
B[0]; // string
B[1]; // number;
B[0|2]; // string|boolean
// 留神以下写法,为什么能够这么写,因为 number 是所有数字的汇合
A[number]; // string
B[number]; // string | number | boolean
A["length"]; // number
B["length"]; // 3
// ts 数组同样能够像 js 数组那样开展
type Spread<T extends unknown[]> = [...T]
Spread<[1,2,3]> // [1,2,3]

依据以上个性,很容易实现以下练习:

// eq1: 实现 `ItemOf` 办法(获取数组中项的类型)type ItemOf<T extends unknown[]> = T[number];
// 之前不晓得这个个性时,用 infer 实现的代码如下
type ItemOfByinfer<T> = T extends (infer N)[] ? N : never;

// eq2: 实现 `Append` 办法
type Append<T extends unknown[], Ele> = [...T, Ele];

// eq3: 实现返回数组 length+1
// ts 尽管无奈实现加减运算,但能够通过模仿生成对应新类型,返回其属性,从而模仿加减运算
type LengthAddOne<T extends unknown[]> = [unknown, ...T]["length"];
四则运算

运算加减依赖于元组长度,因而先定义一些根本办法,留神.. 因为是依赖元组长度,因而无奈算正数和小数,只能算正整数 …

(注:尽管无奈计算正数和小数,但 ts 的 type 仍旧是图灵齐备的,位运算也只是 01 的运算,正数和小数都是一堆 01 的定义,比方把 10000 看做 0,且最初两位是小数,那么 9999 就是 -0.01)

// 返回 Push 后的数组
type Append<T extends unknown[], E = unknown> = [...T, U];
// 同理,返回 Pop 后的数组代码如下,临时用不到
// type RemoveTop<T extends unknown[]> = T extends [...(infer U), unknown] ? U : never;

type Tuple<N extends number, Arr extends unknown[] = []> = Arr["length"] extends N ? Arr : Tuple<N, Append<Arr>>

有了这些根本办法,先实现 加法 减法

type Add<A extends number, B extends number> = [...Tuple<A>, ...Tuple<B>]["length"];

type Subtract<A extends number, B extends number> = Tuple<A> extends [...Tuple<B>, ...(infer U)] ? U["length"] : never;

乘法 的话,A*B就是 AB相加,简易版乘法如下,思路不难,但间接用 Add 和 Subtract 封装,很多写法都提醒嵌套太深。。

留神,这里用于统计和的参数 S 以元组示意,因为所有运算都是以元组为基准,S 用数字示意会先转元组再转数字,来来回回开销比拟大。

type MultipleBase<A extends number, B extends number, S extends unknown[] = []> = 
    B extends 0
    ? S["length"]
    : MultipleBase<A, Subtract<B, 1>, [...S, ...Tuple<A>]>;

乘法还有优化的空间,例如 2*100,间接用这个算的是 100 个 2 相加,工夫复杂度不如100*2, 而计算这么优化的前提是,实现BiggerThan 办法。

type BiggerThan<A extends number, B extends number> = Tuple<A> extends [...Tuple<B>, ...infer U] ? (U["length"] extends (never|0) ? false : true): false;
// 优化后的乘法如下
type Mutiple<A extends number, B extends number> = BiggerThan<A, B> extends true ? MultipleBase<A, B> : MultipleBase<B, A>;

有了 BiggerThan 除法 也好说,例如 a/b,断定b*2、b*3...b*n 和 A 的大小就行。

同乘法的实现,用于统计的参数 R 为元组。。

type Divide<A extends number, B extends number, R extends unknown[] = []> = BiggerThan<B, A> extends true ? R["length"] : Divide<Subtract<A,B>, B, Append<R>>;

至此,四则运算实现结束。

正文完
 0