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

发表于 2年以前  | 总阅读数:346 次

前言

探索经历

我不知道我不知道

我曾经以为 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 类型自己就是一门复杂、独立的语言,那么从我们语言的角度设计这门指南:

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

手册指南

类型变量定义

类型变量的方式有三种,分别为 typeinterfaceenum,他们都相当于 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>
  • 元组:[number, string]
  • 枚举:enum Color{ RED, GREEN, BLUE }
  • any
  • void
  • nullundefined
  • 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、具体值是基础类型的子类型

  • 1number 的子类型
  • trueboolean 的子类型
const a: 1 = 1;
const b: number = a; // ok

const c: true = true;
const d: boolean = c; // ok
复制代码

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

  • 1 | 21 | 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
复制代码

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

5、undefined 在 tsconfig strictNullChecks 为 true 的情况下是 voidany 类型子类型,为 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";
复制代码

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

  • 00268-easy-if[4]

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

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

infer 可以在 X extends Y ? expr1 : expr2Y 中使用类型变量,并且这个类型变量,可以在后续的 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'

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

  • 00189-easy-awaited[5]
  • 03312-easy-parameters[6]
  • 00002-medium-return-type[7]

内置泛型工具

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<First> 继续处理。

  • 如果不是,直接放到返回值的第一个元素。

  • Rest 就是剩下的元素,调用 Flatten<Rest> 继续处理。

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

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

  • 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 拆分为了 FirstRest,其中 First 代表第一个字符,Rest 代表后面所有的字符;
  • 当为 true 时,我们将 FirstRest 分别放入数组中,并将 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 由 TUserGroupByOutputType 共有的属性构成
  • 复杂对象 value 需要判断是否有 _count 属性以及 _count 是否为 boolean 类型,如果都符合,则返回 number,其他属性则返回 GetScalarType<T[P], UserGroupByOutputType<P>>

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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/p_AK8VXJyxiGz4h5pn78zg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录