写点什么

从 0 开始的 TypeScriptの十三:infer、extends、keyof、typeof、in

作者:空城机
  • 2022 年 7 月 14 日
  • 本文字数:3532 字

    阅读完需:约 12 分钟

从0开始的TypeScriptの十三:infer、extends、keyof、typeof、in

在 B 站看视频学习 vue3.0 时,有一节主要是使用 typescript 来配置一些 vuex 的内容


我看完一遍后,还是有挺多困难点的,首先要去了解一下typescript中的inferkeyof等这些高级用法, 所以本文主要是学习 typescript 的记录了。

infer

infertypescript中的关键字,可以在extends条件语句中推断待推断的类型,就是从类型中获得类型


(这里的 extends 不是类、接口的继承,而是对于类型的判断和约束,意思是判断 T 能否兼容)

extends 的示例

type ParamType<T, K> = T extends K ? T : never;
interface Animal { animal: string}interface Cat { cat: string}// ParamType的T需要兼容K,否则会出错let c1: ParamType<Animal | Cat, Cat> = { cat: '猫' }
复制代码



infer 使用

使用方式:


  1. infer 只能在 extends 关键字的右侧

  2. infer x 可以理解成一个未知数 x,表示待推断的函数参数


示例 1: 获取传入的参数类型中的action,如果传入的 T 中没有 action,则会返回 never

type ParamType<T> = T extends { action: infer X } ? X : never;
interface Animal { animal: string, action: void}interface Cat { cat: string, action: ()=>void}// c1的类型void | ()=>voidlet c1: ParamType<Animal | Cat> = ()=>{ console.log('打滚');}c1() // 打滚
复制代码




示例 2: 解包,获取在数组中的元素类型


type ParamType<T> = T extends (infer X)[] ? X : never;// c1类型为number | stringlet c1: ParamType<number[] | string[]> = 10
复制代码




示例 3: 元组 tuple 转联合 union


其实实现的方式和上面是一样的

type ParamType<T> = T extends (infer X)[] ? X : never;// c1类型为number | stringlet c1: ParamType<[string, number]> = 10
复制代码




示例 4: 联合 union 转元组 tuple


这里将 number | string 转换成 number & string的过程就比较复杂了


在这里我也在网上参考了很多文章,才逐步理解的


参考文章:


如果是想的没那么多,那么可能会像下面这样写:


type Change<T> = T extends infer X | infer Y ? [ X, Y ] : nevertype Res = Change<number | string>  // [string, string] | [number, number]
复制代码


这是因为联合类型会分别进行比较。


首先对于extends左边如果是联合类型 union, 那么转换的过程到底应该是怎么样的:

typescript 协变和逆变

这里首先要了解一下typescript的协变和逆变这两个概念


协变(Covariance): 子类型可以赋值给父类型

逆变(Contravariance):父类型可以赋值给子类型


例子:

interface parent {    a: number,}interface child extends parent {    b: number}let p1: parent = {    a: 1,}let p2: child = {    a: 32,    b: 7,}// 协变,可以将子类型赋给父类型,但不能将父类型赋给子类型p1 = p2;  // p2 = p1; 报错// 逆变,将这个特性放到函数类型当中type fun1 = (a: parent)=> voidtype fun2 = (a: child) => voidtype test = fun2 extends fun1 ? true : false
let f1: fun1 = (a: parent)=> {}let f2: fun2 = (a: child)=>{}
// f1 = f2 报错f2 = f1
复制代码


逆变是需要在函数中使用的,除了函数参数类型是逆变,其他都是协变。而在上面联合类型转元组类型中,有一点非常重要,那就是在逆变位置的同一类型变量中的多个候选会被推断成交叉类型


// UnionToTuple = (() => number) & (() => string)type UnionToTuple = ((x: ()=>number) => any) | ((x: ()=>string) => any) extends (x: infer P)  => any ? P : never// Res = [number, string]type Res = UnionToTuple extends { (): infer X; (): infer Y } ? [X, Y] : never
复制代码


通过逆变可以得到以上的方式,这样最后的[number, string]结果就已经得到了,那现在重要的就是得到((x: ()=>number) => any) | ((x: ()=>string) => any)


这一点就比较容易了,以下方式就可以将number | string变成 ((x: { a: string; }) => any) | ((x: { a: number; }) => any)


// number | string// (x: ()=> number)=> any | (x: ()=> string)=> anytype Union<T> = T extends any  ? (x: ()=> T)=> any : never
复制代码


那么最终的转换方式:

type UnionToTuple<T> = ((T extends any  ? (x: ()=> T)=> any : never) extends (x: infer P)  => any ? P : never) extends { (): infer X; (): infer Y } ? [X, Y] : nevertype Res = UnionToTuple<number | string>  // [string, number]
复制代码


emmmm..... 这里的转换过程还是特别复杂的,理解起来也比较麻烦,这里最重要的还是在逆变位置的同一类型变量中的多个候选会被推断成交叉类型,这个概念如果不知道,真的很难推导出来




keyof 索引类型查询操作符

在上面大致了解了 infer 后,继续了解泛型高级类型中的keyof,这个其实有点类似于es6中的keys()方法,用于获取键值的遍历器


keyof可以获取某种类型的所有键,返回联合类型 union


基本使用:

interface User {    name: string    age: number    action: ()=> void}
type usertype = keyof User; // name | age | actionlet t1: usertype = "action"
复制代码


并且,对于 class 类来说,keyof 只能返回类型上已知的公共属性名,在下面的例子当中,keyof产生的也只是name | age | action的联合类型


class User {    name: string;    age: number;    action: ()=> void;    private hobby: ()=> string;    protected eye: string}type usertype = keyof User;  // name | age | action// let t1: usertype = "hobby" // 出错// let t1: usertype = "eye" // 出错
复制代码


如果一个类型有一个symbol或者number类型的索引签名,keyof会直接返回这些类型。


这里的索引签名如果是string类型,那么将会返回string | number,这是在Typescript 2.9中新增的内容,可以参考:https://www.bookstack.cn/read/TypeScript-4.4-zh/zh-release-notes-typescript-2.9.md


type K1 = keyof { [x: symbol]: User }; // symboltype K2 = keyof { [x: number]: User }; // nuumbertype K3 = keyof { [x: string]: User }; // string | number
复制代码


通常keyof在使用时往往会和in或者typeof搭配使用



typeof

typeof 是用来判断数据类型,返回成员的类型 可以对对象枚举函数进行类型返回


  • 示例: 对象

// typeof 对象let A = {    a: 'aaa',    b: 1111}/*type _A = {    a: string;    b: number;}*/type _A = typeof A
复制代码


  • 示例: 类


// typeof 类class C {    a: number;    b: string}
type _C = typeof C let c: _C = C // emmm.... 感觉好像没什么意义
复制代码


然后我上网搜索了一下,发现如果是下面这种情况,是需要使用typeof重新获取类的


class Ponit {    x: number;    y: number;    constructor(x: number, y: number) {      this.x = x;      this.y = y;    }};// 工厂函数function getInstance(PointClass: typeof Ponit, x: number, y: number) {    return new PointClass(x, y);}// 下面写法将报错function getInstance2(PointClass: Ponit, x: number, y: number) {    return new PointClass(x, y);// 报错 此表达式不可构造。类型 "Ponit" 没有构造签名。}
复制代码


  • 示例: 枚举


// typeof 枚举// 使用枚举限定日期enum day { Mon, Tue, Wed, Thu, Fri, Sat, Sun}type _day = typeof day;let days: _day = {    Mon: 4,    Tue: 12,    Wed: 1,     Thu: 1,     Fri: 1,     Sat: 1,     Sun: 1}console.log(days); // { Mon: 4, Tue: 12, Wed: 1, Thu: 1, Fri: 1, Sat: 1, Sun: 1 }
复制代码


  • 示例:函数


function compare(x: number, y: number):boolean {    return x > y;}type _compare = typeof compare;  // (x: number, y: number) => boolean
复制代码




in 类型映射

对于类型,同样也可以进行遍历枚举,使用的方式就是in关键字


使用方式: [ K in Keys ] , 这里的 Keys 必须是string,number,symbol或者联合类型


示例:将type A = { name: number; age: number; } 内部类型全部从number转变为string


运营之前学到的keyof,将类型A转变为name | age, 然后再使用in遍历此联合类型,分别对属性名分配类型


type A = {    name: number;    age: number;}// type User = { name: string; age: string; }type User = {    [K in keyof A]: string}
复制代码


其实关于泛型的类型转换还有内置类型可以使用,这里就先不说明了

发布于: 刚刚阅读数: 6
用户头像

空城机

关注

曾经沧海难为水,只是当时已惘然 2021.03.22 加入

业余作者,在线水文 主要干前端的活,业余会学学python 欢迎各位关注,互相学习,互相进步

评论

发布
暂无评论
从0开始的TypeScriptの十三:infer、extends、keyof、typeof、in_typescript_空城机_InfoQ写作社区