【问题标题】:Intersection Types from variable array of types来自可变类型数组的交集类型
【发布时间】:2019-10-29 17:22:38
【问题描述】:

这是Extend class with Plugin Architecture的后续问题

我有一个类Test 有一个静态方法.plugin,它接受一个函数。该函数可以运行任意代码并扩展Tests API。

const FooTest = Test.plugin(FooPlugin)
const fooTest = new FooTest()
fooTest.foo()

FooPlugin 在创建FooTest 时返回一个对象{ foo(): 'foo' },它与Test 的原型相结合。那部分正在工作。

现在我希望静态.plugin() 方法也接受一个插件数组,所以我不需要每次调用.plugin() 方法时都创建一个新类,而只需创建一次。返回的类应将返回类型数组缩减为单个对象,并将其与Tests 原型合并。

这是我希望实现的结果代码

const FooBarTest = Test.plugin([FooPlugin, BarPlugin])
const fooBarTest = new FooBarTest()
fooBarTest.foo()
fooBarTest.bar()

这可能吗?我创建了一个TypeScript Playground,第 15-16 行需要更改才能使这一切成为可能。感谢您的帮助!

【问题讨论】:

    标签: typescript


    【解决方案1】:

    如果有人感兴趣,这就是我找到解决方案的方法。

    切换类型参数

    这项任务的挑战是不仅要从单个函数中提取返回类型,还要从函数数组中提取返回类型。我需要一个条件类型,它可以接受单个 TestPlugin 或其中的一个数组,并根据提供的内容产生两种不同的结果:

    • 如果提供了单个 TestPlugin,请提取其返回类型。
    • 如果提供了TestPlugin[],则创建所有返回类型的交集。

    由于我想到的条件类型将接受TestPlugin | TestPlugin[] 的联合,因此我必须在plugin 方法的范围内声明一个。让我们从这里切换定义:

    static plugin<T extends TestPlugin>(plugin: T | T[])
    

    到这里:

    static plugin<T extends (TestPlugin | TestPlugin[])>(plugin: T)
    

    现在我们有了一个可以使用的T

    构建条件类型

    条件类型的最终版本如下所示:

    type ReturnTypeOf<T extends AnyFunction | AnyFunction[]> =
      T extends AnyFunction
        ? ReturnType<T>
          : T extends AnyFunction[]
            ? UnionToIntersection<ReturnType<T[number]>>
            : never
    

    要实现它,我需要两种辅助类型。第一个是AnyFunction。这使得ReturnTypeOf 可以与任何函数一起使用,而不仅仅是TestPlugin,但如果我们不打算在其他地方重用ReturnTypeOf,我们也可以使用TestPlugin

    type AnyFunction = (...args: any) => any;
    

    另一种类型是伟大的@jcalz著名的转变。如果T 是一个函数数组,并且数组按数字索引,那么T[number] 是该数组所有成员的并集。它被称为查找类型

    我们可以在函数数组上调用ReturnType(并获得它们返回类型的联合),但这不是我们想要的。我们想要他们的交集,而不是他们的联合。 UnionToIntersection 会为我们做这件事。

    /**
     * @author https://stackoverflow.com/users/2887218/jcalz
     * @see https://stackoverflow.com/a/50375286/10325032
     */
    type UnionToIntersection<Union> =
      (Union extends Unrestricted
        ? (argument: Union) => void
        : never
      ) extends (argument: infer Intersection) => void // tslint:disable-line: no-unused
          ? Intersection
          : never;
    

    用我们自定义的ReturnTypeOf替换ReturnType

    当我们在Test.plugin 范围内检查T 的类型时,我们注意到它总是一个插件或插件数组。即使是类型保护 (Array.isArray(plugin)) 也无法帮助 TypeScript 区分该联合。而且由于内置的​​ReturnType 不能接受数组,我们需要用我们自定义的ReturnTypeOf 替换ReturnType 的两个实例。

    最终解决方案

    type ApiExtension = { [key: string]: any }
    type TestPlugin = (instance: Test) => ApiExtension;
    type Constructor<T> = new (...args: any[]) => T;
    
    /**
     * @author https://stackoverflow.com/users/2887218/jcalz
     * @see https://stackoverflow.com/a/50375286/10325032
     */
    type UnionToIntersection<Union> =
      (Union extends any
        ? (argument: Union) => void
        : never
      ) extends (argument: infer Intersection) => void // tslint:disable-line: no-unused
          ? Intersection
          : never;
    
    type AnyFunction = (...args: any) => any;
    
    type ReturnTypeOf<T extends AnyFunction | AnyFunction[]> =
      T extends AnyFunction
        ? ReturnType<T>
          : T extends AnyFunction[]
            ? UnionToIntersection<ReturnType<T[number]>>
            : never
    
    class Test {
      static plugins: TestPlugin[] = [];
      static plugin<T extends TestPlugin | TestPlugin[]>(plugin: T) {
        const currentPlugins = this.plugins;
    
        class NewTest extends this {
          static plugins = currentPlugins.concat(plugin);
        }
    
        if (Array.isArray(plugin)) {
          type Extension = ReturnTypeOf<T>
          return NewTest as typeof NewTest & Constructor<Extension>;  
        }
    
        type Extension = ReturnTypeOf<T>
        return NewTest as typeof NewTest & Constructor<Extension>;
      }
    
      constructor() {
        // apply plugins
        // https://stackoverflow.com/a/16345172
        const classConstructor = this.constructor as typeof Test;
        classConstructor.plugins.forEach(plugin => {
          Object.assign(this, plugin(this))
        });
      }
    }
    
    const FooPlugin = (test: Test): { foo(): 'foo' } => {
      console.log('plugin evalutes')
    
      return {
        foo: () => 'foo'
      }
    }
    const BarPlugin = (test: Test): { bar(): 'bar' } => {
      console.log('plugin evalutes')
    
      return {
        bar: () => 'bar'
      }
    }
    
    const FooTest = Test.plugin(FooPlugin)
    const fooTest = new FooTest()
    fooTest.foo()
    
    const FooBarTest = Test.plugin([FooPlugin, BarPlugin])
    const fooBarTest = new FooBarTest()
    fooBarTest.foo()
    fooBarTest.bar()
    

    TypeScript Playground

    【讨论】:

    • 干得好!如果plugin 函数定义必须保持与上述问题相同,您甚至可以直接将默认ReturnTypeUnionToIntersection 一起使用。 sample(游乐场网址不适合此处)
    • 正确!对于 TypeScript,这两种情况是相同的,因此无需创建两个分支。很好的收获。
    【解决方案2】:

    来自 WrocTypeScript 的 Karol 发现了这一点:Playground

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-05-30
      • 1970-01-01
      • 2015-07-10
      • 1970-01-01
      • 2011-06-13
      • 1970-01-01
      • 2022-11-16
      • 2017-06-12
      相关资源
      最近更新 更多