最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 如何更容易上手 Typescript 类型计算?

    正文概述 掘金(ELab)   2021-02-03   458

    Why

    import * as fs from "fs";
    function promisify(fn) {
      return function(...args) {
        return new Promise((resolve, reject) => {
          fn(...args, (err, data) => {
            if(err) {
              return reject(err);
            }
            resolve(data);
          });
        });
      }
    }
    
    (async () => {
      let file = await promisify(fs.readFile)("./xxx.json");
    })();
    

    这个问题的答案在实战演练环节会结合本文的内容给出答案,如果你觉得这个问题简单得很,那么恭喜你,你已经具备本文将要介绍的大部分知识点。

    1. 我们需要知道 promisify(fs.readFile)(...) 这里能够接受的类型。
    2. 我们需要知道 let file = await ... 这里 file 的类型。

    如何让类似于 promisify这样的函数保留类型信息是“体操”或者我称之为类型计算的意义所在。

    前言 (Preface)

    最近在国内的前端圈流行一个名词“TS 体操”,TS 对应 TypeScript,“体操”是 TC 的戏称,而 TC 就是 turing completeness (图灵完备)的意思,具体就是指验证 TypeScript 的 Type-level programming (编译时(compile time)的相关语法)是图灵完备的,经过传播之后,也泛指以一个 TS 类型作为输入,通过写 TS 代码输出另外一个类型的实践。

    TypeScript 可能已经被验证时图灵完备的:

    1. www.zhihu.com/question/41…

    不过笔者认为上述概念在前端圈可能比较小众、“体操”这个名字对于外行人来说相对难以与具体的行为对应起来、目前整个 TC 过程更像有趣的 brain teaser,所以笔者觉得 TC “体操”还是用 Type Computing 、Type Calculation 或者“类型计算”来记忆会比较好理解,这也容易与具体行为对应,本文在接下来的环节会用“类型计算”来取代“体操”说法。

    建模 (Modeling)

    其实类型计算说白了就是写程序,这个程序接受类型作为输入,然后输出另一个类型,因此可以把它建模成写普通的程序,并按照一般计算机语言的组成部分对 TS 的类型相关语法进行归类。

    语法分类 (Grammar Classification)

    首先我们看看基本的语言都有哪些语法结构,以 JS 为例,从 AST(抽象语法树)的角度来看,语法可以按照以下层级结构进行分类:

    如何更容易上手 Typescript 类型计算?

    但是我们今天不会以这种从上到下的树状结构来整理和学习,这样子的学习曲线一开始会比较陡峭,所以作者并没有按照从上到下的顺序来整理,而是以学习普通语言的语法顺序来整理。

    基本类型 (Basic Types)

    类似于 JS 里面有基本类型,TypeScript 也有基本类型,这个相信大家都很清楚,TypeScript 的基本类型如下:

    • Boolean
    • Number
    • String
    • Array
    • Tuple (TypeScript 独有)
    • Enum (TypeScript 独有)
    • Unknown (TypeScript 独有)
    • Any (TypeScript 独有)
    • Void (TypeScript 独有)
    • Null and Undefined
    • Never (TypeScript 独有)
    • Object

    任何复杂类型都是基本类型的组合,每个基本类型都可以有具体的枚举:

    type A = {
        attrA: string,
        attrB: number,
        attrA: true, // Boolean 的枚举
        ...
    }
    

    函数 (Function)

    Javascript 中有函数的概念,那么 TypeScript 的 Type-level programming(以下简称 TP) 相关语法中有没有函数的概念呢?答案是有的,带范型的类型就相当于函数。

    // 函数定义
    type B<T> = T & {
        attrB: "anthor value"
    }
    
    // 变量
    class CCC {
    ...
    }
    type DDD = {
    ...
    }
    
    // 函数调用
    type AnotherType = B<CCC>;
    type YetAnotherType = B<DDD>;
    

    其中 <T> 就相当于函数括弧和参数列表,= 后面的就相当于函数定义。或者按照这个思路你可以开始沉淀很多工具类 TC 函数了,例如

    // 将所有属性变成可选的
    type Optional<T> = {
      [key in keyof T]?: T[key];
    }
    
    // 将某些属性变成必选的
    type MyRequired<T, K extends keyof T> = T &
      {
        [key in K]-?: T[key];
      };
      
    // 例如我们有个实体
    type App = {
      _id?: string;
      appId: string;
      name: string;
      description: string;
      ownerList: string[];
      createdAt?: number;
      updatedAt?: number;
    };
    
    // 我们在更新这个对象/类型的时候,有些 key 是必填的,有些 key 是选填的,这个时候就可以这样子生成我们需要的类型
    type AppUpdatePayload = MyRequired<Optional<App>, '_id'>
    

    上面这个例子又暴露了另外一个可以类比的概念,也就是函数的参数的类型可以用 <K extends keyof T> 这样的语法来表达。

    TypeScript 函数的缺陷 (Defect)

    目前下面这三个缺陷笔者还没有找到办法克服,聪明的你可以尝试看看有没有办法克服。

    高版本才能支持递归

    4.1.0 才支持递归

    函数不能作为参数

    在 JS 里面,函数可以作为另外一个函数的入参,例如:

    function map(s, mapper) { return s.map(mapper) }
    map([1, 2, 3], (t) => s);
    

    但是在类型计算的“函数”里面,暂时没有相关语法能够实现将函数作为参数传入这种形式,正确来说,传入的参数只能作为静态值变量引用,不能作为可调用的函数。

    type Map<T, Mapper> = {
      [k in keyof T]: Mapper<T[k]>; // 语法报错
    }
    
    支持闭包,但是没有办法修改闭包中的值

    TypeScript 的“函数中”目前笔者没有找到相关语法可以替代

    type ClosureValue = string;
    
    type Map<T> = {
      [k in keyof T]: ClosureValue; // 笔者没有找到语法能够修改 ClosureValue
    }
    

    但是我们可以通过类似于函数式编程的概念,组合出新的类型。

    type ClosureValue = string;
    
    type Map<T> = {
      [k in keyof T]: ClosureValue & T[k]; // 笔者没有找到语法能够修改 ClosureValue
    }
    

    语句 (Statements)

    在 TypeScript 中能够对应语句相关语法好像只有变量声明语句相关语法,在 TypeScript 中没有条件语句、循环语句函数、专属的函数声明语句(用下述的变量声明语句来承载)。

    变量声明语句 (Variable Declaration)

    变量声明在上面的介绍已经介绍过,就是简单地通过 type ToDeclareType = Expresion 这样子的变量名加表达式的语法来实现,表达式有很多种类,我们接下来会详细到介绍到,

    type ToDeclareType<T> = T extends (args: any) => PromiseLike<infer R> ? R : never; // 条件表达式/带三元运算符的条件表达式
    type ToDeclareType = Omit<App>; // 函数调用表达式
    type ToDeclareType<T>= { // 循环表达式
        [key in keyof T]: Omit<T[key], '_id'>
    }
    

    表达式 (Expressions)

    带三元运算符的条件表达式 (IfExpression with ternary operator)

    我们在 JS 里面写“带三元运算符的条件表达式”的时候一般是 Condition ? ExpressionIfTrue : ExpressionIfFalse 这样的形式,在 TypeScript 中则可以用以下的语法来表示:

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

    其中 T extends (args: any) => PromiseLike<infer R> 就相当条件判断,R : never 就相当于为真时的表达式和为假时的表达式。

    利用上述的三元表达式,我们可以扩展一下 ReturnType,让它支持异步函数和同步函数

    async function hello(name: string): Promise<string> {
      return Promise.resolve(name);
    }
    // type CCC: string = ReturnType<typeof hello>; doesn't work
    type MyReturnType<T extends (...args) => any> = T extends (
      ...args
    ) => PromiseLike<infer R>
      ? R
      : ReturnType<T>;
    type CCC: string = MyReturnType<typeof hello>; // it works
    

    函数调用/定义表达式 (CallExpression)

    在上述“函数”环节已经介绍过

    循环相关 (Loop Related)(Object.keys、Array.map等)

    循环实现思路 (Details Explained )

    TypeScript 里面并没有完整的循环语法,循环是通过递归来实现的,下面是一个例子:

    type IntSeq<N, S extends any[] = []> =
        S["length"] extends N ? S :
        IntSeq<N, [...S, S["length"]]>
    

    理论上下面介绍的这些都是函数定义/表达式的一些例子,但是对于对象的遍历还是很常见,用于补全循环语句,值得单独拿出来讲一下。

    对对象进行遍历 (Loop Object)
    type AnyType = {
      [key: string]: any;
    };
    type OptionalString<T> = {
      [key in keyof T]?: string;
    };
    type CCC = OptionalString<AnyType>;
    
    对数组(Tuple)进行遍历 (Loop Array/Tuple)
    map
    const a = ['123', 1, {}];
    type B = typeof a;
    type Map<T> = {
      [k in keyof T]: T[k] extends (...args) => any ? 0 : 1;
    };
    type C = Map<B>;
    type D = C[0];
    
    reduce
    const a = ['123', 1, {}];
    type B = typeof a;
    type Reduce<T extends any[]> = T[number] extends (...arg: any[]) => any ? 1 : 0;
    type C = Reduce<B>;
    

    注意这里的 reduce 返回的是一个 Union 类型。

    成员表达式 (Member Expression)

    我们在 JS 中用例如 a.b.c 这样的成员表达式主要是因为我们知道了某个对象/变量的结构,然后想拿到其中某部分的值,在 TypeScript 中有个比较通用的方法,就是用 infer 语法,例如我们想拿到函数的某个参数就可以这么做:

    function hello(a: any, b: string) {
      return b;
    }
    type getSecondParameter<T> = T extends (a: any, b: infer U) => any ? U : never;
    type P = getSecondParameter<typeof hello>;
    

    其中 T extends (a: any, b: infer U) => any 就是在表示结构,并拿其中某个部分。

    当然其中 TypeScript 本身就有一些更加简单的语法

    type A = {
      a: string;
      b: string;
    };
    type B = [string, string, boolean];
    type C = A['a'];
    type D = B[number];
    type E = B[0];
    // eslint-disable-next-line prettier/prettier
    type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
    type F = Last<B>;
    

    常见数据结构和操作 (Common Datastructures and Operations)

    Set

    集合数据结构可以用 Union 类型来替代

    Add
    type S = '1' | 2 | a;
    S = S | 3;
    
    Remove
    type S = '1' | 2 | a;
    S = Exclude<S, '1'>;
    
    Has
    type S = '1' | 2 | a;
    type isInSet = 1 extends S ? true : false;
    
    Intersection
    type SA = '1' | 2;
    type SB = 2 | 3;
    type interset = Extract<SA, SB>;
    
    Diff
    type SA = '1' | 2;
    type SB = 2 | 3;
    type diff = Exclude<SA, SB>;
    
    Symmetric Diff
    type SA = '1' | 2;
    type SB = 2 | 3;
    type sdiff = Exclude<SA, SB> | Exclude<SB, SA>;
    
    ToIntersectionType
    type A = {
      a: string;
      b: string;
    };
    type B = {
      b: string;
      c: string;
    };
    type ToIntersectionType<U> = (
      U extends any ? (arg: U) => any : never
    ) extends (arg: infer I) => void
      ? I
      : never;
    type D = ToIntersectionType <A | B>;
    
    ToArray
    type Input = 1 | 2;
    type UnionToIntersection<U> = (
      U extends any ? (arg: U) => any : never
    ) extends (arg: infer I) => void
      ? I
      : never;
    type ToArray<T> = UnionToIntersection<(T extends any ? (t: T) => T : never)> extends (_: any) => infer W
      ? [...ToArray<Exclude<T, W>>, W]
      : [];
    type Output = ToArray<Input>;
    

    注意:这可能是 TS 的 bug 才使得这个功能成功,因为 :

    type C = ((arg: any) => true) & ((arg: any) => false);
    type D = C extends (arg: any) => infer R ? R : never; // false;
    

    但在逻辑上,上述类型 C 应该是 never 才对,因为你找不到一个函数的返回永远是 true 又永远是 false。

    Size
    type Input = 1 | 2;
    type Size = ToArray<Input>['length'];
    

    Map/Object

    Merge/Object.assign
    type C = A & B;
    
    Intersection
    interface A {
      a: string;
      b: string;
      c: string;
    }
    interface B {
      b: string;
      c: number;
      d: boolean;
    }
    type Intersection<A, B> = {
      [KA in Extract<keyof A, keyof B>]: A[KA] | B[KA];
    };
    type AandB = Intersection<A, B>;
    
    Filter
    type Input = { foo: number; bar?: string };
    type FilteredKeys<T> = {
      [P in keyof T]: T[P] extends number ? P : never;
    }[keyof T];
    type Filter<T> = {
      [key in FilteredKeys<T>]: T[key];
    };
    type Output = Filter<Input>;
    

    Array

    成员访问
    type B = [string, string, boolean];
    type D = B[number];
    type E = B[0];
    // eslint-disable-next-line prettier/prettier
    type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
    type F = Last<B>;
    type G = B['length'];
    
    Append
    type Append<T extends any[], V> = [...T, V];
    
    Pop
    type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never
    
    Dequeue
    type Dequeue<T extends any[]> = T extends [infer _, ...infer I] ? I : never
    
    Prepend
    type Prepend<T extends any[], V> = [V, ...T];
    
    Concat
    type Concat<T extends any[], V extends any[] > = [...T, ...V];
    
    Filter
    type Filter<T extends any[]> = T extends [infer V, ...infer R]
      ? V extends number
        ? [V, ...Filter<R>]
        : Filter<R>
      : [];
    type Input = [1, 2, string];
    type Output = Filter<Input>;
    
    Slice
    type Input = [string, string, boolean];
    type Slice<N extends number, T extends any[]> = T['length'] extends N
      ? T
      : T extends [infer _, ...infer U]
      ? Slice<N, U>
      : never;
    type Out = Slice<2, Input>;
    

    这里只用一层循环实现 Array.slice(s) 这种效果,实现 Array.slice(s, e) 涉及减法,比较麻烦,暂不在这里展开了。

    运算符 (Operators)

    基本原理 (Details Explained)

    基本原理是通过 Array 的 length 属性来输出整型,如果要实现 * 法,请循环加法 N 次。。。

    type IntSeq<N, S extends any[] = []> =
        S["length"] extends N ? S :
        IntSeq<N, [...S, S["length"]]>;
    

    ===

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

    +

    type NumericPlus<A extends Numeric, B extends Numeric> = [...IntSeq<A>, ...IntSeq<B>]["length"];
    

    -

    type NumericMinus<A extends Numeric, B extends Numeric> = _NumericMinus<B, A, []>;
    type ToNumeric<T extends number> = T extends Numeric ? T : never;
    type _NumericMinus<A extends Numeric, B extends Numeric, M extends any[]> = NumericPlus<A, ToNumeric<M["length"]>> extends B ? M["length"] : _NumericMinus<A, B, [...M, 0]>;
    

    其他 (MISC)

    inferface

    有些同学可能会问 interface 语法属于上述的哪些范畴,除了 Declaration Merging,interface 的功能都可以用 type 来实现,interface 更像是语法糖,所以笔者并没有将 interface 来实现上述任意一个功能。

    inteface A extends B {
        attrA: string
    }
    
    Utility Types

    TypeScript 本身也提供了一些工具类型,例如取函数的参数列表有 Parameters 等,具体可以参照一下这个链接。

    实战演练 (Excercise)

    Promisify

    import * as fs from "fs";
    function promisify(fn) {
      return function(...args: XXXX) {
        return new Promise<XXXX>((resolve, reject) => {
          fn(...args, (err, data) => {
            if(err) {
              return reject(err);
            }
            resolve(data);
          });
        });
      }
    }
    (async () => {
      let file = await promisify(fs.readFile)("./xxx.json");
    })();
    
    1. 我们需要知道 promisify(fs.readFile)(...) 这里能够接受的类型。
    2. 我们需要 let file = await ... 这里 file 的类型。
    答案

    结合类型计算和新版本 TS,会比官方实现库更简洁、更具扩展性(只支持 5 个参数) github.com/DefinitelyT…

    import * as fs from "fs";
    // 基于数据的基本操作 Last 和 Pop
    type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
    type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never;
    // 对数组进行操作
    type GetParametersType<T extends (...args: any) => any> = Pop<Parameters<T>>;
    type GetCallbackType<T extends (...args: any) => any> = Last<Parameters<T>>;
    // 类似于成员变量取值
    type GetCallbackReturnType<T extends (...args: any) => any> = GetCallbackType<T> extends (err: Error, data: infer R) => void ? R : any;
    function promisify<T extends (...args: any) => any>(fn: T) {
      return function(...args: GetParametersType<T>) {
        return new Promise<GetCallbackReturnType<T>>((resolve, reject) => {
          fn(...args, (err, data) => {
            if(err) {
              return reject(err);
            }
            resolve(data);
          });
        });
      }
    }
    (async () => {
      let file = await promisify(fs.readFile)("./xxx.json");
    })();
    

    MyReturnType

    基本上就是成员表达式部分提到的通用的提取某个部分的实现方法(用 infer 关键字)

    const fn = (v: boolean) => {
      if (v) return 1;
      else return 2;
    };
    type MyReturnType<F> = F extends (...args) => infer R ? R : never;
    type a = MyReturnType<typeof fn>;
    

    Readonly 2

    基本上就是 Merge 和遍历 Object

    interface Todo {
      title: string;
      description: string;
      completed: boolean;
    }
    type MyReadonly2<T, KEYS extends keyof T> = T &
      {
        readonly [k in KEYS]: T[k];
      };
    const todo: MyReadonly2<Todo, 'title' | 'description'> = {
      title: 'Hey',
      description: 'foobar',
      completed: false,
    };
    todo.title = 'Hello'; // Error: cannot reassign a readonly property
    todo.description = 'barFoo'; // Error: cannot reassign a readonly property
    todo.completed = true; // O
    

    Type Lookup

    成员访问和三元表达式的应用

    interface Cat {
      type: 'cat';
      breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal';
    }
    interface Dog {
      type: 'dog';
      breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer';
      color: 'brown' | 'white' | 'black';
    }
    type LookUp<T, K extends string> = T extends { type: string }
      ? T['type'] extends K
        ? T
        : never
      : never;
    type MyDogType = LookUp<Cat | Dog, 'dog'>; // expected to be `Dog`
    

    Get Required

    参照 Object 的 Filter 方法

    type GetRequiredKeys<T> = {
      [key in keyof T]-?: {} extends Pick<T, key> ? never : key;
    }[keyof T];
    type GetRequired<T> = {
      [key in GetRequiredKeys<T>]: T[key];
    };
    type I = GetRequired<{ foo: number; bar?: string }>; // expected to be { foo: number }
    

    想法 (Thoughts)

    沉淀类型计算库 (Supplementary Utility Types)

    除了 Utility Types 之外,添加通用的,易于理解的 TypeScript 工具类库,做 TS 届的 underscore。

    Update: 发现已经有这样的库了:

    • github.com/piotrwitek/…
    • github.com/sindresorhu…

    直接用 JS 做类型计算 (Doing Type Computing in Plain TS)

    即使按照本文的建模方式,由上面的归类可以看出,目前对比起现代的编程语言还是缺失挺多的关键能力。类型计算学习成本太高、像智力游戏的原因也是因为语法成分缺失、使用不直观的原因。为了使类型计算面向更广的受众,应当提供更友好的语法、更全面的语法,一个朴素的想法是在 compile time 运行的类似 JS 本身的语法(宏?)。

    以下语法纯粹拍脑袋,例如:

    type Test = {
        a: string
    }
    typecomp function Map(T, mapper) {
        for (let key of Object.keys(T)) {
            T[key] = mapper(T[key]);       
        }
    }
    typecomp AnotherType = Map(Test, typecomp (T) => {
        if (T extends 'hello') {
            return number;
        } else {
            return string;
        }
    });
    

    如果有这样子直观的语法,笔者感觉会使得类型计算更容易上手。需要实现这样的效果,可能需要我们 fork TypeScript 的 repo,添加以上的功能,希望有能力的读者可以高质量地实现这个能力,效果好的话,还可以 merge 到源 TypeScript Repo 中,造福笔者这个时刻为类型计算苦恼的开发者。

    Reference

    1. github.com/type-challe…
    2. www.zhihu.com/question/41…
    3. github.com/piotrwitek/…

    ❤️ 谢谢支持

    1. 喜欢的话别忘了 分享、点赞、收藏 三连哦~。

    起源地下载网 » 如何更容易上手 Typescript 类型计算?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元