Skip to content

Typescript 类型编程,从入门到念头通达 #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wenzi0github opened this issue Sep 13, 2022 · 4 comments
Open

Typescript 类型编程,从入门到念头通达 #10

wenzi0github opened this issue Sep 13, 2022 · 4 comments

Comments

@wenzi0github
Copy link
Owner

wenzi0github commented Sep 13, 2022

Typescript 类型编程,从入门到念头通达

超杰_ 高级前端进阶 2022-08-29 09:00 发表于安徽

前言

探索经历

我不知道我不知道

我曾经以为 Tyepscript 只是在 Javascript 基础上加一些类型注释,是 JavaScript 的增强版而已,属于有手就会。

我知道我不知道

直到我用到了 Prisma 这个 NodeJS ORM 工具,其生成的类型定义,可以根据你的参数,完美应对关联查询、部分查询等各种场景,完全吊打其他 NodeJS ORM。出于好奇,我看了以下源码:图片

这还是我认识的 Typescript 吗???

我只知道我知道的

后面浏览 Github Trending[1] 时发现了 type-challenges[2] 这个项目(俗称类型体操)。

图片
微信截图_20220806230248.png
然后就开始了我的 TS 编程挑战,但在刷的过程中,我发现已经做过的,似乎感觉会了,但到下一题还是写不出来,甚至过几天重刷还会忘记怎么写。

图片
154390DB.png

我不知道我知道的

然后我停止刷题,开始思考他们到底有什么规律,我似乎顿悟了:

TS 类型本身就是一个很复杂的、独立的语言,不仅仅是 JS 的增强和类型注释。

然后我就尝试着从语言的层面理解 TS 类型,瞬间豁然开朗,仿佛进入了桃花源,那些类型挑战不过是这些基础知识的应用而已,再也不用死记硬背。

图片
0070XTeOgy1gtv57eag5fj606l05y0sq02.jpg

学习建议

要有一定的 TS 基础,起码有一两个项目用过 TS;
先去刷 type-challenges[3],接受毒打,然后自己总结,然后再看本文,本文每节知识后面都对应着能解决挑战,在拿着知识的冲锋枪去挑战它;
学习本文时,一定要打开宇宙第一的 VSCode,创建一个 TS 文件,将示例代码拷贝进去看看效果
如果还没做完以上准备,可以先收藏一下本文,毕竟收藏了就是学会了。

学习目标

  • 能够深入理解 TS 类型编程的相关知识
  • 能挑战完大部分 type-challenges 题
  • 能理解开源项目中的 Typescript 定义

大纲

前面我们说了 TS 类型自己就是一门复杂、独立的语言,那么从我们语言的角度设计这门指南:

  • 类型变量定义
  • 类型数据和值
  • 类型的父子关系
  • 循环语句
  • 递归语句
  • 字符串操作
  • 对象操作
  • 元组操作
  • 条件语句

手册指南

类型变量定义

类型变量的方式有三种,分别为 type、interface、enum,他们都相当于 JS 中的 const,一旦定义就不可改变,三者的区别是:

  • enum:仅用来定义枚举类型;
  • interface:可以用来定义函数、对象、类;
  • type:使用绝大多数类型,例如普通的值、对象、函数、数组、元组等。

例如:

/** type 方式定义 */
type A = string; // 普通类型
type B = number[]; // 数组
type C = (num: number) => number; // 函数
type D = { age: number; name: string }; // 对象

/** enum */
enum Color {
  GREEN,
  RED,
  BLUE,
}

/** interface 定义 */
interface Sum {
  // 函数(没必要这样定义,除非你的函数有其他属性)
  (num1: number, num2: number): number;
}
const getSum: Sum = (a: number, b: number) => a + b;

interface Person {
  // 对象
  name: string;
  age: number;
}

类型数据和值

在官方文档中第一篇就介绍了 TS 的基础类型包括了:

  • 布尔:boolean
  • 数字:number
  • 字符串:string
  • 数组:number[] / Array
  • 元组:[number, string]
  • 枚举:enum Color{ RED, GREEN, BLUE }
  • any
  • void
  • null、undefined
  • never
  • object

但是这里要问一下大家,除了这些难道就没有其他的值了吗???

大家请看下面的例子:

type A = 1;
const a: A = 1; //完全正确

type B = { name: "zhang"; age: 18 };
const b: B = { name: "zhang", age: 18 }; // 毫无报错

type C = [1, number, 2, string];
const c: C = [1, 111, 2, "hello"]; // 没任何毛病

type S = `num - ${A}`; // 'num - 1' 可以使用字符串模板,简直离谱

从上面的例子大家也可以得出一个结论:

JS 中合法的值,在 TS 类型中同样合法,也就是 TS 类型的值 = TS 基础类型 + JS 值,并且可以混用。

类型的父子关系

类型是有父子关系的,子类型的值可以赋值给父类型,但是父类型的值是不能够赋值给子类型的。例如:

type ParentType = 1 | 2 | string;
type SubType = 1;

let parentData: ParentType = 2;
let subData: SubType = 1;

subData = parentData; // ❌ 父类型不能赋值给子类型的值
parentData = subData; // 🆗

这一特性对于后面要讲的泛型和条件判断有着至关重要的作用,我们先简单看一下类型中的条件语句:

type IsSub = SubType extends ParentType ? true : false; // IsSub 的类型值为 true

了解了父子类型的基本概念后,我们还需要掌握在 类型数据和值 中提到的各种 TS 类型之间的父子关系,为后面学习泛型、条件、递归等打下基础。

1、具体值是基础类型的子类型

  • 1 是 number 的子类型
  • true 是 boolean 的子类型
const a: 1 = 1;
const b: number = a; // ok

const c: true = true;
const d: boolean = c; // ok

2、联合类型中的部分是整体的子类型

  • 1 | 2 是 1 | 2 | 3 的父类型

3、never 类型是所有类型的子类型

function foo(): never {
  throw new Error();
}

const a: 1 = foo(); // 可以赋值,类型不会报错就证明了 never 类型是 1 的子类型

4、对象判断子类型,需要逐个属性比较

type ButtonProps = {
  size: "mini" | "large";
  type: "primary" | "default";
};

type MyButtonProps = {
  size: "mini";
  type: "primary" | "default";
  color: "red" | "blue";
};

type IsSubButton = MyButton extends Button ? true : false; // true

在进行比较时,首先 MyButtonProps 的 size 和 type 都是 ButtonProps 中对应属性的子类型,虽然 MyButtonProps 比 ButtonProps 多了个 size ,但其不参与比较。

5、undefined 在 tsconfig strictNullChecks 为 true 的情况下是 void 和 any 类型子类型,为 false 的情况下则除 never 的子类型

// strictNullChecks 为 true(默认行为)

let a: undefined;

let b: number = 1;
let c: void;
let d: any = "jack";

b = a; // ❌ undefined 不是其他类型子类型
c = a; // 🆗 undefined 是 void 类型子类型
d = a; // 🆗 undefined 是 any 类型子类型
// strictNullChecks 为 false

let a: undefined;

let b: number = 1;
let c: void;

b = a; // 🆗 undefined 是其他类型子类型
c = a; // 🆗 undefined 是 void 类型子类型
d = a; // 🆗 undefined 是 any 类型子类型

6、undefined 在 tsconfig strictNullChecks 为 true 的情况下是 any 类型子类型,为 false 的情况下则除 never 的子类型

父子关系与联合类型

当子类型与父类型组成联合类型时,实际效果等于父类型。例如:

type A = number | 1; // number
type B = never | string; // string (never 前面说了是所有类型的子类型)

变量取属性

我们知道在 JS 中对象是可以通过 . 操作符,而在 TS 类型中,也能进行相似的操作。例如:

// 普通对象
interface Person {
  name: string;
  age: number;
}
type Name = Person["name"]; // string

// enum 枚举
enum Color {
  Red,
  Green,
  Blue,
}
type Red = Color.Red; // 0

// 数组(数组是没法获取 length 属性的,因为有多少项是不固定的)
type Names = string[];
type FirstName = Names[0]; // string
type Len = Names["length"]; // ❌

// 元组(元组是可以获取 length 属性的,因为其长度是固定的)
type Language = ["js", "java", "python", "rust"];
type Rust = Language[3]; // rust
type Len = Language["length"]; // 🆗

// 字符串
type Str = "hello";
type S = Str[0]; // ⚠️ 注意是 string,不是 h
type StrLen = Str["length"]; // number 而非具体的数字

⚠️ 注意,基础类型是可以取到原型的定义的,所以并非无属性。

// 字符串原型方法
type Concat = "h"["concat"]; // String.prototype.concat 的类型定义

// 数字原型方法
type N = 1;
type ToFixed = 1["toFixed"]; // Number.prototype.toFixed 的类型定义

获取类型所有属性 key

想要知道对象有哪些属性,可以使用 keyof 关键词。例如:

interface Person {
  name: string;
  age: number;
}

type Keys = keyof Person; // 返回属性的联合联合类型

⚠️ 箭头函数类型和空对象没有 key。例如:

type F = () => void;
type K = keyof F; // never;
type Foo = keyof {}; // never;

条件语句

TS 类型编程中并没有其他语言中的 if/else 语法,而是使用了三元运算符 X extends Y ? expr1 : expr2。

  • X extends Y − 判断 X 是否为 Y 的子类型
  • expr1 − 如果 X 是 Y 的子类型,则返回该值
  • expr2 − 如果 X 不是 Y 的子类型,则返回该值
type A = 1 extends number ? 1 : never; // 1
type IsRed = "blue" extends "red" ? true : false; // false

类型中的函数(泛型)

泛型基础和定义

TS 泛型就像 JS 的函数一样,可以根据输出的类型,决定返回的类型。我们看一个简单的例子:

// JS 函数
function foo(arg) {
  return arg;
}

// TS 泛型
type Foo<T> = T;

在 foo 函数作用是,你给他什么值,它就返回什么值;Foo 泛型则是你给他什么类型,它返回什么类型。

除了上面的定义方式,还可以使用 interface 定义。例如:

interface FormData<T> {
  name: string;
  data: T;
}

泛型约束

我们写 JS 函数的时候,为了代码的健壮性,通常会对输入参数进行校验,泛型中通过 extends 关键字也实现了类似的功能。例如:

class Person {
  name: string;
}
function getName(user) {
  if (!(user instanceof Person)) {
    throw new Error(`${user} is not instanceof "Person"`);
  }
  return user.name;
}

type GetName<U extends Person> = U["name"];

泛型参数默认值

我们知道 ES6 后函数支持参数默认值,同样的,在 TS 类型编程中,泛型也有默认值的能力。例如:

// js 函数参数默认值
function getSum(a = 0, b = 0) {
  return a + b;
}
const sum = getSum(); // 0

// TS 泛型默认值
type UnionType<T = number, U = string> = T | U;
type MyType = UnionType; // string | number

泛型与条件判断

上面的示例中,我们只列举了简单的场景,当配合条件语句的时候,泛型的灵活性就更大了。例如:

type IsBoolean<T> = T extends boolean ? true : false;

type A = IsBoolean<true>; // true
type B = IsBoolean<1>; // false;
复制代码;
// 嵌套条件语句
type Upper<T extends string> = T extends "a" ? "A" : T extends "b" ? "B" : T; // 嵌套了另一个条件语句

type B = Upper<"b">; // "B";

学完本小节,你可以试着挑战:

泛型与条件与类型推断变量

如果以上介绍的内容对你来说虽然既陌生又熟悉,接下来我们引入的一个关键词你可能从未听过,即 infer。

infer 可以在 X extends Y ? expr1 : expr2 的 Y 中使用类型变量,并且这个类型变量,可以在后续的 expr1 中使用。

例如我们需要得到函数的返回值的类型可以如下操作:

type ReturnType<T> = T extends (...args: any) => infer R ? R : never;

type GetSum = (a: number, b: number) => number;

type A = ReturnType<GetSum>; // number;

其中 R 既类型推断变量。

extends 是一个大忙人:

  • 在 JS 中,担当类的继承重担,例如 App extends Component
  • 在 TS 类型中,当泛型约束,例如 type ToUpper<S extends string> = xxx
  • 在 TS 类型中,条件判断的关键词 type ReturnType<T> = T extends () => infer R ? R : never'

学完本小节,你能完成的挑战:

内置泛型工具

Typescript 给我们内置了一些极其有用的泛型工具,我们本小节挑一些简单说明:

type Person = {
  name: string;
  age: number;
  id: number;
};

// Pick 挑选出指定属性,生成新对象类型
type UserInfo = Pick<Person, "name" | "age">; // 挑选出 { name: string; age: number }

// Omit 排除指定的属性,生成新的对象类型
type UserInfo2 = Omit<Person, "id">; // 排除 id,生成 { name: string; age: number }

// Partial 将对象所有属性变为可选
type PartialPerson = Partial<Person>; // { name?: string; age?: number; id?: number }

// Readonly 将对象所有属性变为只读
type ReadonlyPerson = Readonly<Person>; // { readonly name: string; readonly age: number; readonly id: number }

// Record 生成对象类型,例如
type PersonMap = Record<number, Person>; // { [index: number]: Person }

// Exclude 排除一些联合类型
type UserInfoKeys = Exclude<keyof Person, "id">; // 'name' | 'age'

对象类型的操作

属性修饰

对象的属性是可以有修饰符的,目前有两种修饰符,分别是 readonly 关键字对应的可选属性 和 ?: 对应的可选属性。例如:

type Person = {
  name: string;
  age?: number; // 1、可选属性
  readonly id: number; // 2、只读属性
};

const person: Person = {
  name: "张三",
  id: 1,
  // 没有 age 属性,不报错,说明 age 可选
};

person.id = 2; // 报错,不能修改只读属性
person.age = 18; // 正常

属性修饰与父子关系

父子类型主要讨论属性的存在与否,所以:

  • 可选类型会导致父子关系的出现,因为可选类型相当于 自身 | undefined,其是必填类型 自身 的父类型
  • readonly 则不会导致对象之间存在父子关系
type A = { name: string };
type B = { name?: string };

let a: A = { name: "a" };
let b: B = {};

a = b; // ❌ B 类型中 name 是 `string |undefined`, 是 A 类型中 `string` 的父类型,所以不能赋值
b = a; // OK,string 可以赋值给 `string |undefined`
type A = { name: string };
type B = { readonly name: string };

let a: A = { name: "a" };
let b: B = { name: "b" };

a = b; // OK
b = a; // OK

a.name = "abc"; // OK,a 的类型还是 A,所以可以修改

对象类型的遍历

我们知道在 JS 中可以使用 for/in 遍历对象的属性,在 TS 类型编程中也有类似的方式,不过更加简洁。

例如我们把所有的属性都加上 readonly 修饰符:

// js 对象遍历
const person = {
  name: "张三",
  age: 18,
  id: 1,
};

for (const key in person) {
  console.log(key, person[key]);
}

// ts 类型对象遍历
type Person = {
  name: string;
  age?: number;
  readonly id: number;
};

type Readonly<T> = {
  readonly [Key in keyof T]: T[Key];
};

上述示例中有以下几点:

  • keyof 关键字可以获取到对象所有的属性(前面讲过)
  • Key 就是每次遍历时存的属性名
  • T[Key] 就是每次遍历时存的属性值
  • 然后我们在每个属性前面加上了 readonly 这样所有的属性都是只读的了。

学完本小结,你可以试着挑战:

00004-easy-pick[8]
00007-easy-readonly[9]
00003-medium-omit[10]
00008-medium-readonly-2[11]
00009-medium-deep-readonly[12]

元组类型的操作

只读修饰符 & 父子关系

在元组中也可以像对象那样在元组前面加上 readonly 代表元组的每一项都是只读的。例如:

type Arr = readonly [1, number];
const a: Arr = [1, 2];
a[0] = 3; // Error: readonly

如果两个类型元素完全相同 的前提下,只读的和非只读是有父子关系的:非只读是只读的子类型。具体没查到原因,不过可以理解自我催眠为 readonly 表示的更信息更多。

type A = readonly [1, 2, 3];
type B = [1, 2, 3];

let a: A = [1, 2, 3];
let b: B = [1, 2, 3];

a = b; // 非只读可以赋值给只读的
b = a; // ❌ 只读的元组不能赋值给非只读的

学完本节你可以搞定:

00018-easy-tuple-length

元组的解构

元组的解构和 JS 数组的解构十分相似。假设我们需要将两个元组类型合并成一个,我们可以如下操作:

// JS 合并两个数组
function concat(arr1, arr2) {
return [...arr1, ...arr2);
}
const arr = concat([1], [2, 3]); // [1,2,3]

// TS 合并两个元组
type Concat<T extends any[], U extends any[]> = [...T, ...U];

type Result = Concat<[1], [2, 3]> // [1, 2, 3]

readonly 的元组转为非 readonly,我们可以使用解构完成。例如:

type A = readonly [number, string];
type B = [...A]; // 变成了非 readonly 了

const b: B = [1, "a"];
b[0] = 2;

因为 readonly 是针对整个元组而言的,所以通过解构,我们就将每个元素取出来了,重新赋值给另一个类型变量就解决这个问题了。

学完本节,你可以完成如下挑战:

00014-easy-first[13]
00533-easy-concat[14]
03057-easy-push[15]
03060-easy-unshift[16]
00015-medium-last[17]
00016-medium-pop[18]
00191-medium-append-argument[19]

元组的遍历

有些情况下,我们需要对每个元素进行判断和处理,此时就需要使用元组的遍历,元组的遍历有两种方式:

  • 递归方式
  • 对象类型遍历方式
递归方式遍历

我们以 多维元组拍平为一维元组 为例,来看看怎么用递归的思想实现。

// JS 中用递归思想解决数组拍平问题
function flatten(arr) {
  if (arr.length === 0) return [];
  const [first, ...rest] = arr;
  if (Array.isArray(first)) {
    return [...flatten(first), ...flatten(rest)];
  }
  return [first, ...flatten(rest)];
}
const a = flatten([1, [[2]]]); // [1, 2]

// TS 中用递归思想解决元组拍平问题
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : [];

type a = Flatten<[1, [[2]]]>; // [1,2]

相信能看懂 JS 逻辑的人都能看懂 TS 逻辑,两者几乎一致:

  • 首先我们先判断是否能够结构为符合要求的元组,如果不能,直接返回空元组。
  • 如果能,我们判断元组的第一个元素是否为元组
    • 如果是,调用 Flatten 继续处理。
    • 如果不是,直接放到返回值的第一个元素。
  • Rest 就是剩下的元素,调用 Flatten 继续处理。

最终返回的类型就是通过递归拍平的元组类型了。

学完本小结,你可以解决:

00898-easy-includes[20]
00459-medium-flatten[21]
00949-medium-anyof[22]

对象类型遍历方式

我们再用示例说明如何使用对象类型遍历方式处理元组。

// JS 示例:希望 [1, () => 2+3, 4] 能够被处理成 [1, 5, 4]
function getArrVal(arr) {
  for(let key in arr) {
    if (typeof arr[key] === 'function') {
      arr[key] = arr[key]();
    }
  }
  return arr;
}

// Ts 示例:希望 [1, () => number, string] 能够被处理成 [1, number, string]
// 对象遍历的方式
type GetType<T extends any[]> = {
  [K in keyof T]: T[K] extends () => infer R ? R : T[K]
}

// 递归的处理方式
type GetType<T extends any[]> = T extends [infer First, ..]

总结:

  • 两种处理方式都能遍历到每个元素,并对每个元素做一些判断和处理。
  • 除了在会增加元素数量(上面的 Flatten 示例)的情况下,必须使用递归的模式,其它情况可任选。

学完本节,你可以挑战:

00020-medium-promise-all[23]
00527-medium-append-to-object[24]
00599-medium-merge[25]

元组与索引与联合类型

元组其实就是个数有限、类型固定的数组类型。所以前面也讲过,其可以使用数字作为下标来访问的,例如:

type tupleStr = ["a", "b", "c"];
type A = tupleStr[0]; // 'a'
type B = tupleStr[1]; // 'b'

如果这个索引是 number 会发生什么呢?

type tupleStr = ["a", "b", "c"];
type UnionStr = tupleStr[number]; // 'a' | 'b' | 'c' 变成了联合类型

因为 number 代表了可能是 0 也可能是 1 或者 2,所以这些可能性组成的集合就是联合类型。

学完本节你应该可以挑战:

00011-easy-tuple-to-object[26]
00010-medium-tuple-to-union[27]

字符串操作

字符串的相关操作主要体现在两方面:

  • 字符字符字面量类型的解构
  • 字符串字面量类型的遍历

字符串类型推导和解构

字符串类型推导和解构,是将一个完整字符串分解为几个部分,然后对各个部分我们可以进行各种处理。

这里需要注意的是,在拆分的时候需要注意是否含有字符串字面量作为分割符,有和没有的情况,分割后的变量含义并不相同。

推导类型中有字符串字面量的情况

我们需要实现一个将字符串类型中 _ 去除的功能,其可以为:

type DelUnderline<T extends string> =
  T extends `${infer LeftWords}_${infer RightWords}`
    ? `${LeftWords}${RightWords}`
    : T;

// 测试用例
type HelloWorld = DelUnderline<"hello*world">; // helloworld(LeftWords 为 hello,RightWords 为 world)
type World = DelUnderline<"_world">; // world(LeftWords 为空字符串,RightWords 为 world)
type Hello = DelUnderline<"hello*">; // hello(LeftWords 为 hello,RightWords 为空字符串)

我们从上面例子可以得到结论:

当推断类型中有字符串字面量作为边界时,如上例的 _,其解构的左边 LeftWords 是左侧所有字符串的代表,右边 RightWords 是右侧所有字符串的代表,并且可以代表空字符串。

学完本节,你可以挑战:

00106-medium-trimleft[28]
00108-medium-trim[29]
00116-medium-replace[30]
00119-medium-replaceall[31]
00529-medium-absolute[32]

推导类型中无字符串字面量的情况

假设我们要实现 TS 类型的首字母大写的效果,我们可以这样写:

type MyCapitalize<T extends string> = T extends `${infer First}${infer Rest}`
  ? `${Uppercase<First>}${Rest}`
  : T;

type A = MyCapitalize<"hello">; // "Hello"(First 为 "h",Rest 为 "ello")
type B = MyCapitalize<"b">; // "B" (First 为 "h",Rest 为空字符串)
type C = MyCapitalize<"">; // 当为空字符串时,会走到 false 的分支,返回空字符串

我们从上面例子可以得到结论:

当推断类型中没有字符串字面量作为边界时,第一个变量作为第一个字符,第二个变量代表剩下的字符,可以为空字符串。当然如果有三个变量,${A}${B}${C},则第一个变量 A 代表第一个字符,B 代表第二个字符串,C 代表剩下的字符。

学完本节,你可以挑战:

00110-medium-capitalize[33]
00531-medium-string-to-union[34]

字符串字面量类型的遍历

字符串字面量类型的遍历,核心是使用递归思想以及上面提到的字符串的解构,这在后面很多转换中都很重要。

这里我们使用字符串类型转元组类型小试牛刀:

type StringToTuple<T extends string> = T extends `${infer First}${infer Rest}`
  ? [First, ...StringToTuple<Rest>]
  : [T];

type Foo = StringToTuple<"abc">; // ["a", "b", "c"]

我们这里分析一下:

  • 首先我们将 T 拆分为了 First 和 Rest,其中 First 代表第一个字符,Rest 代表后面所有的字符;
  • 当为 true 时,我们将 First 和 Rest 分别放入数组中,并将 Rest 进行递归的拆分,且将拆分后的数组使用数组解构,打平成一维数组;
  • 当为 false 时,此时 T 为空字符串,我们返回一个空数组,而不应该是空字符串。
    学完本节,你能够挑战:

00298-medium-length-of-string[35]
00612-medium-kebabcase[36]
00645-medium-diff[37]

联合类型的操作

联合类型与泛型推导

联合类型代表着几种可能性的集合,它在泛型推导中和其他类型都不一样,你可以把他理解为它在做泛型推到时,并不是一次性判断,而是将每一项单独判读并返回,然后再将这些返回进行联合。

说起来有点绕,我们看下面的例子就明白了:

type Foo<T> = T extends "a" | "b" ? `${T}1` : T;

type Bar = Foo<"a" | "b" | "c">; // "a1" | "b1" | "c"

如我们上面说的,例子中并不是将 'a' | 'b' | 'c' 一次性判断的,而是:

  • 先判断 a ,走到 true 分支,返回 "a1"
  • 然后判断 b ,走到 true 分支,返回 "b1"
  • 最后判断 c ,走到 false 分支,返回 "c"
  • 再将 "a1"、"b1" "c" 联合成 "a1" | "b1" | "c" 并返回。

为了更加清楚的明白,我们再举一个例子:

interface Cat {
  type: "🐱";
  food: string[];
}
interface Dog {
  type: "🐶";
  food: string[];
}
type Animal = Cat | Dog;

type LookUp<U, T> = U extends { type: T } ? U : never;

type A = LookUp<Animal, "🐶">; // Dog

根据前面说的,它会将联合类型的每个成员拿去比较,最后返回。所以其判断步骤为:

  • Cat 与 {type: '🐶'},会走到 false 分支,返回 never;
  • Dog 与 {type: '🐶'},会走到 true 分支,返回 Dog;
  • Dog | never 根据前面讲过的联合类型与父子关系,最终返回的就是 Dog

学完本小结,你可以解决:

00043-easy-exclude[38]
00062-medium-type-lookup[39]
00296-medium-permutation[40]

其他

从 JS 值转为 TS 值

我们知道 TS 是有类型推导的,即便是一个没定义类型的 JS 变量也是有其类型定义的,此时我们是可以通过 typeof 完成从 JS 到 TS 的转化的。例如:

// 定义 JS 变量
const jack = {
  name: "jack",
  age: 18,
};

// 从 JS 中获取 TS 并赋值
type Person = typeof jack; // 此时 Person 为 { name: string; age: number };

应用场景:

这种情况多用在,我们需要时使用开源库的类型时(比如泛型传参),它又没做类型定义或者没导出的场景,例如 redux-toolkit 文档示例[41]。

更精准的类型推测

有人就会好奇,为什么 name 会被推导为 string,而不是 "jack" 呢?

默认情况下 TS 对对象或者数组的推导是尽可能宽泛的,想要让其具体,需要使用到 as const 语法,让其尽可能精准的推测。例如:

const jack = {
  name: "jack",
  age: 18,
} as const; // 会被推导为:{ readonly name: "jack"; readonly age: 18; }

const arr = [1, 2] as const; // 会被推导为:readonly [1, 2];

要注意,每个元素都是 readonly 的哦。

总结与回顾

经过奋斗,大家终于来到了终点,我们以始为终,先看看最初定下的目标有没有实现:

  • 能够深入理解 TS 类型编程的相关知识
  • 能挑战完大部分 type-challenges 题
  • 能理解开源项目中的 Typescript 定义

就我个人而言,在学习和挑战 type-challenges 过程中是对 TS 类型有了更深入的了解,那么最后我们再看一下开头提到的 Prisma 类型定义到底说的什么意思。

  • 首先它是一个泛型,接受一个参数 T,并且这个参数需要符合 UserGroupByArgs 这个类型
  • 返回一个 PrismaPromise 包裹的 Array
  • 数组的每个元素是由 PickArray<UserGroupByOutputType, T['by']> 以及另一个复杂对象构成
  • 复杂对象 key 由 T 和 UserGroupByOutputType 共有的属性构成
  • 复杂对象 value 需要判断是否有 _count 属性以及 _count 是否为 boolean 类型,如果都符合,则返回 number,其他属性则返回 GetScalarType<T[P], UserGroupByOutputType

    >

到这里,终于算是结束了,散会,撒花 ✿✿ ヽ(°▽°)ノ ✿

关于本文
作者:超杰_
https://juejin.cn/post/7132490947320872974

@wenzi0github
Copy link
Owner Author

TS 类型本身就是一个很复杂的、独立的语言,不仅仅是 JS 的增强和类型注释。

@wenzi0github
Copy link
Owner Author

Pick, Omit一般是操作对象属性;而 Extract, Exclude 一般是操作用type声明的联合类型。也不知道我理解的对不对?

@wenzi0github
Copy link
Owner Author

如何限制子组件的类型,如中组件中的children,只能是之类的:

export const Case: React.FC<CaseProps> = props => {
  return <>{props.children}</>;
};

export const When = ({ children }: { children: ReturnType<typeof Case> | ReturnType<typeof Case>[] }) => {};

@wenzi0github
Copy link
Owner Author

React中的children如何用 function 来实现,然后传递参数?

interface TabItemProps {
  label: string;
  children: React.ReactNode | ((props: { loading: boolean; result: any }) => React.ReactNode);
}

export const TabItem = ({ label, children, request }: TabItemProps) => {
  const [loading, setLoading] = useState(true);
  const [result, setResult] = useState<any>(null);

  useEffect(() => {
    if (typeof request === 'function') {
      setLoading(true);
      Promise.resolve(request()).then((res: any) => {
        setLoading(false);
        setResult(res);
      });
    }
  }, [request]);

  return (
    <View className="tab-item" data-label={label}>
      {typeof children === 'function' ? children({ loading, result }) : <>{children}</>}
    </View>
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant