类型的可变数组中的交点类型

时间:2019-10-29 17:22:38

标签: typescript

这是对Extend class with Plugin Architecture

的跟进问题

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

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

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

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

这是我想要的结果代码

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

那有可能吗?我已经创建了TypeScript Playground,第15-16行需要进行更改以使其全部变为可能。感谢您的帮助!

2 个答案:

答案 0 :(得分:2)

万一有兴趣的人,这就是我得出解决方案的方式。

切换类型参数

此任务的挑战是不仅要从单个函数中提取返回类型,还要从函数数组中提取返回类型。我需要一个条件类型,该条件类型可以接受一个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,而且如果我们不打算在其他地方重用TestPlugin,我们也可以使用ReturnTypeOf

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;

用我们的自定义ReturnType替换ReturnTypeOf

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

最终解决方案

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

答案 1 :(得分:0)

WrocTypeScript的Karol知道了:Playground