1. 首页
  2. typescript深入探索

万万没想到,TypeScript居然还能这么玩

ts内置类型

Partial

将其变为可选

 typescript
type Partial<T> = {
    [P in keyof T]?: T[P];
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

这里稍微解释一下

keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值.

例子:

 typescript
interface People {name: string}
// 变为
Partial<People> => {name?: string}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

其是有局限性的,只能处理一层

 typescript
interface People {
    name: string;
    person: {name: string; name1: string}
}
type NewPeople = Partial<People>
// error: Property 'name1' is missing in type ...
const jack: NewPeople = {
  name: 'jack',
  person: {
    name: 'son'
  }
}
// 如何解决呢 递归
type PowerPartial<T> = {
    [U in keyof T]?: T[U] extends object
      ? PowerPartial<T[U]>
      : T[U]
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Readonly

只读

 typescript
type Readonly<T> {readonly [P in keyof T]: T[P]}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

当然这也只能一层 如上面Partial例子来看jack.person.name 是可以直接修改的。 也可以和Partial结合起来

 typescript
type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] };
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Required

 typescript
type Required<T> = {
    [P in keyof T]-?: T[P];
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

上面的-?, 这里很好理解就是将可选项代表的 ? 去掉, 从而让这个类型变成必选项. 与之对应的还有个+? , 这个含义自然与-?之前相反, 它是用来把属性变成可选项的.

Pick

从 T 中取出 一系列 K 的属性

 typescript
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

例子:

 typescript
type NewPerson = Pick<People, 'name'>; // { name: string; }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Exclude

从 T 中移除 一系列 U 的属性

 typescript
type Exclude<T, U> = T extends U ? never : T;
// demo
type T = Exclude<1 | 2, 1 | 3> // => 2
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

与 Exclude 类似的 Extract<T, U>(取交集)

 typescript
type Extract<T, U> = T extends U ? T : never;
type T = Extract<'a'|'b'|'c'|'d' ,'b'|'c'|'e' > // => // 'b'|'c'
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

将Pick 和 Exclude 结合起来实战

 typescript
// 比如后端接口定义好的返回类型是这个,但是我们并不能直接修改
interface Api {
    name: string;
    age: number
}
// error: Types of property 'name' are incompatible.
interface CustomApi extends Api {
  name: number;
}
// change
interface CustomApi1 extends Pick<Chicken, 'age'> {
  name: number;
}
// 但是上面还是太复杂了,你需要把所有属性挑拣起来,结合 Exclude 将key全拿出来 可以省事很多
interface CustomApi2 extends Pick<Api, Exclude<keyof Api, 'name'>> {
  name: number;
}
// 上述其实 就是Omit的源码
interface CustomApi3 extends Omit<Api, 'name'> {
  name: number;
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

类似Exclude作用的 还有 NonNullable,将 null | undefined排除

 typescript
type NonNullable<T> = T extends null | undefined ? never : T;
// demo
type Test = '111' | '222' | null;
type NewTest = NonNullable<Test>; // '111' | '222'
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Omit

未包含

 typescript
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
// demo
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Record

标记对象的 key value类型

 typescript
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
// demo
const user: Record<'name'|'email', string> = {
    name: '',
    email: ''
}
// 复杂一点
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>;
// 这里简易实现,否则报ts(2391)错误
function mapObject(): any {}
const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);
type newNames =  typeof lengths  // => { foo: number, bar: number, baz: number }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

ReturnType

反解

 typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

其实这里的 infer R 就是声明一个变量来承载传入函数签名的返回值类型(反解), 简单说就是用它取到函数返回值的类型方便之后使用. 举个例子来理解infer

 typescript
// 反解Promise类型
type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;
// demo
async function stringPromise() {
  return "string promise";
}
type extractStringPromise = UnPromisify<typeof stringPromise>; // string
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

ReturnType demo

 typescript
// demo
function TestFn() {
  return 'test';
}
type Test = ReturnType<typeof TestFn>; // => string
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

和上述差不多了 我们可以依葫芦画瓢 个 PromiseType

 typescript
type PromiseType<T extends Promise<any>> = T extends Promise<infer R>  ? R  : never;
// demo
type Test = PromiseType<Promise<string>> // => string
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

再结合深入一点

 typescript
type PromiseReturnType<T extends () => any> = ReturnType<T> extends Promise<
  infer R
>
  ? R
  : ReturnType<T>

async function test() {
  return { a: 1, b: '2' }
}

type Test = PromiseReturnType<typeof test> // Test 的类型为 { a: number; b: string }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

Parameters

获取一个函数的所有参数类型

上面的ReturnType认识了infer,这里直接放源码和demo了

 typescript
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
// demo
interface IPerson {name: string}
interface IFunc {
  (person: IPerson, count: number): boolean
}
type P = Parameters<IFunc> // => [IPerson, number]
const person1: P[0] = {
  name: '1'
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

ConstructorParameters

类似于 Parameters<T>, ConstructorParameters 获取一个类的构造函数参数

 typescript
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
// demo
type DateConstrParams = ConstructorParameters<typeof Date>  // => string | number | Date
//  这里补充一下,源码中Date构造器定义
interface DateConstructor {
    new (value: number | string | Date): Date;
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

业务结合,自定义

1. 有的时候重写属性

 typescript
interface Test {
  name: string;
  age: number;
}
// error: Type 'string | number' is not assignable to type 'string'
interface Test2  extends Test{
  name: Test['name'] | number
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

实现 将T中的key对应的value设为any 其就不冲突了

 typescript
type Weaken<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? any : T[P];
}

interface Test2  extends Weaken<Test, 'name'>{
  name: Test['name'] | number
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

当然上面更极端的做法也可以是 排除再重写

 typescript
interface Test2 extends Omit<Test, 'name'> {
    name: Test['name'] | number
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

2. 将联合类型|转成交叉类型&

 typescript
type UnionToIntersection<U> = (U extends any
  ? (k: U) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never
type Test = UnionToIntersection<{ a: string } | { b: number }> // => { a: string } & { b: number }
// 但是 我们可以例外推断个例子
type Weird = UnionToIntersection<string | number | boolean> // => never
// 因为不可能有一个字符串和数字以及真与假的值。
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

可能有人会懵,首先得理解Distributive conditional types,举个例子 T extends U ? X : Y 中,当 TA | B 时,会拆分成 A extends U ? X : Y | B extends U ? X : Y;再结合infer同一类型变量的多个候选类型将会被推断为交叉类型,参考举个例子:

 typescript
// 这是转为联合类型
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number
// 这是转为交叉类型
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

综上:

 typescript
type Result = UnionToIntersection<T1 | T2>; // => T1 & T2
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

  • 第一步:(U extends any ? (k: U) => void : never) 会把 union 拆分成 (T1 extends any ? (k: T1) => void : never) | (T2 extends any ? (k: T2)=> void : never),即是得到 (k: T1) => void | (k: T2) => void
  • 第二步:((k: T1) => void | (k: T2) => void) extends ((k: infer I) => void) ? I : never,根据上文,可以推断出 I 为 T1 & T2

3. 数组 转换 成 union

 typescript
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const; // TS 3.4
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number];  // union type : 'hearts' | 'diamonds' | 'spades' | 'clubs'
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

ts3.4 新语法 as const创建不可变(常量)元组类型/数组,所以TypeScript可以安全地采用窄文字类型['a', 'b']而不是更宽('a' | 'b')[]或甚至string[]类型

4. 合并参数返回值类型

 javascript
function injectUser() {
  return { user: 1 }
}
function injectBook() {
  return { book: '2' }
}
const injects = [injectUser, injectBook]
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

 typescript
// 利用第2个点的拓展 UnionToIntersection
// 将联合类型汇总
type Prettify<T> = [T] extends [infer U] ? { [K in keyof U]: U[K] } : never
type InjectTypes<T extends Array<() => object>> = T extends Array<() => infer P>
  ? Prettify<UnionToIntersection<P>>
  : never

type result = InjectTypes<typeof injects> // Test 的类型为 { user: number, book: string }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

上面看似一堆,我们可以一步步将其拆解

 typescript
type test = typeof injects; // =>  ((() => { user: number;}) | (() => {  book: string;}))[]
type test1<T> =  T extends Array<() => infer P> ? P : never
type test2 = test1<test> // =>{user: number;} | {  book: string;}
// 这一步就可以用UnionToIntersection 转为联合类型
type test3 = UnionToIntersection<test2>
type test4 = Prettify<test3> // =>{ user: number, book: string }
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

其实上面的过于复杂的,我们可以采用另一个方案

 typescript
type User = ReturnType<typeof injectUser>
type Book = ReturnType<typeof injectBook>
type result = Prettify<User & Book>
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

最终可以改成这样

 typescript
type User = ReturnType<typeof injectUser>
type Book = ReturnType<typeof injectBook>
type result = User | Book
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

5. 常用内置类型结合封装

  • PartialRecord
interface Model {
    name: string;
    age: number;
}
interface Validator {
    required?: boolean;
    trigger?: string;
}
// 定义表单的校验规则
const validateRules: Record<keyof Model, Validator> = {
    name: {required: true, trigger: `blur`}
    // error: Property age is missing in type...
}
// 解决
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>

const validateRules: PartialRecord<keyof Model, Validator> = {
   name: {required: true, trigger: `blur`}
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

  • DeepPartial
// 处理数组 再处理对象
type RecursivePartial<T> = {
  [P in keyof T]?:
    T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object ? RecursivePartial<T[P]> :
    T[P];
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

  • DeepRequired
type DeepRequired<T> = {
  [P in keyof T]-?:
  T[P] extends ((infer U)[]|undefined) ? DeepRequired<U>[] :
  T[P] extends (object|undefined) ? DeepRequired<T[P]> :
  T[P]
}

6. 挑选出readonly字段

 typescript
type IfEquals<X, Y, A=X, B=never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;

type ReadonlyKeys<T> = {
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
}[keyof T];

type A = {
  readonly a: string
  b: number
}
type B = ReadonlyKeys<A> // => 'a'
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

以下参考轮子哥的解释:首先应该解释一下ReadonlyKeys里面[Q in P]的意思。P他是一个字符串,不是一个字符串的集合,所以[Q in P]实际上就是P。如果你直接写{P:T[P]}的话,你得到的是一个拥有成员变量"P"的对象,而{[Q in P]:T[P]}拿到的是变量P在这里的值(也就是”a”或者”b”),而且他还把有没有readonly的这个属性给带了回来。如果把ReadonlyKeys改成这样的类型type

 typescript
type ReadonlyKeys2<T> = {
    [P in keyof T]-?: { [Q in P]: T[P] }
};
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

那我们会得到为

 typescript
ReadonlyKeys2<A> 为 {
    readonly a: {readonly a:string};
    b: {b:number};
}
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

然后我们就去调用IfEquals。在这里我需要指出,<T>()=>T extends X ?1:2的优先级是<T>()=>(T extends X ?1:2),在T是个自由变量的情况下,我们比较的是X和Y究竟是不是同一个类型。比较两个泛型类型,又没有办法拿到确切的值来计算,只能直接比较一下表达式是否相同。

 typescript
{
    readonly a : (<T>()=>T extends {readonly a:string} ? 1 : 2) extends (<T>()=>T extends {a:string} ? 1 : 2) ? never : 'a';
             b : (<T>()=>T extends {b:number} ? 1 : 2) extends (<T>()=>T extends {b:number} ? 1 : 2) ? never : 'b';
}['a' | 'b']
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

ps: 这里提一下 -readonly其实就是将 T 的所有属性的 readonly 移除

然后我们来计算一下显然,以下两个表达式是不一样的<T>()=>T extends {readonly a:string} ? 1 : 2<T>()=>T extends {a:string} ? 1 : 2而以下两个表达式是一样的<T>()=>T extends {b:number} ? 1 : 2<T>()=>T extends {b:number} ? 1 : 2一样会得到never,不一样就得到一个字符串,这里原理是Conditional Types在有变量没被resolve时,extends部分必须完全一致才算相等 于是上面的类型就被简化为

 typescript
{
    readonly a:'a';
             b:never;
}['a' | 'b']
// js的对象取值联合类型
/* 给大家推荐一个面试刷题:Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/special/leetcode */

显然never就代表没有,因此你得到了’a’。 如果您阅读之后有所帮助,欢迎点赞,如果有更好的意见和批评,欢迎指出!

作者:晓黑板前端技术
链接:https://juejin.im/post/6895538129227546638

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:万万没想到,TypeScript居然还能这么玩

链接:https://www.javascriptc.com/4770.html

« Vue3实战之Vue3.0 + Vant3.0 搭建种子项目
CentOS yum源安装 Nginx»
Flutter 中文教程资源

相关推荐

QR code