这是对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行需要进行更改以使其全部变为可能。感谢您的帮助!
答案 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()
答案 1 :(得分:0)
WrocTypeScript的Karol知道了:Playground