如果有人感兴趣,这就是我找到解决方案的方法。
切换类型参数
这项任务的挑战是不仅要从单个函数中提取返回类型,还要从函数数组中提取返回类型。我需要一个条件类型,它可以接受单个 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