在 TypeScript 中映射通用扩展参数类型
Mapping generic spreaded argument types in TypeScript
我正在尝试编写一个通用方法,它接受任意数量的作为对象键的参数,并将它们的键的值用作构造函数的参数。这是我的原始实现:
// Typescript 2.x
export function oldMethod<TProps>() {
function create<
TInstance extends Geometry | BufferGeometry,
>(
geometryClass: new () => TInstance,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1]) => TInstance,
key1: TKey1,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
TKey2 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2]) => TInstance,
key1: TKey1,
key2: TKey2,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
TKey2 extends Extract<keyof TProps, string>,
TKey3 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2], param3: TProps[TKey3]) => TInstance,
key1: TKey1,
key2: TKey2,
key3: TKey3,
): any;
// ...all the way up to 8 possible keys
function create<TInstance extends Geometry | BufferGeometry>(
geometryClass: new (...args: Array<TProps[Extract<keyof TProps, string>]>) => TInstance,
...args: Array<Extract<keyof TProps, string>>) {
class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
protected constructGeometry(props: TProps): TInstance {
return new geometryClass(...args.map((arg) => props[arg]));
}
}
return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
TProps,
TInstance,
GeometryContainerType> {
constructor() {
super(GeneratedGeometryWrapper, geometryClass);
this.hasRemountProps(...args);
}
};
}
return create;
}
随着 TypeScript 3.0 宣布使用元组提取和传播参数列表,我希望我能够删除重载以使其更简单:
// Typescript 3.x
export function newMethod<TProps>() {
function create<TInstance extends Geometry | BufferGeometry, TArgs extends Array<Extract<keyof TProps, string>>>(
geometryClass: new (...args: /* what goes here? */) => TInstance,
...args: Array<Extract<keyof TProps, string>>) {
class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
protected constructGeometry(props: TProps): TInstance {
return new geometryClass(...args.map((arg) => props[arg]));
}
}
return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
TProps,
TInstance,
GeometryContainerType> {
constructor() {
super(GeneratedGeometryWrapper, geometryClass);
this.hasRemountProps(...args);
}
};
}
return create;
}
但是,我不知道将什么作为定义构造函数类型的 args
的类型。如果我能够像 JavaScript 中的对象那样操作类型,我会这样写:...[...TArgs].map(TArg => TProps[TArg]
,但显然这不是有效的 TypeScript 语法,我想不出任何方法来做到这一点.我是否缺少表达这种类型的方法?有没有什么方法可以使它完全类型安全,而不必重载函数和有限数量的参数?是否缺少一些允许我表达这种类型的 TypeScript 功能?
我为下面的例子去掉了很多代码,但应该是一样的精神。
您缺少的功能在 2018 年 8 月的某个时候称为 mapped arrays/tuples which is planned to be released in TypeScript 3.1。您将能够像映射其他类型一样映射数组和元组,如下所示:
type Mapped<T> = {[K in keyof T]: Array<T[K]>};
type Example = Mapped<[string, number, boolean]>;
// type Example = [string[], number[], boolean[]];
如果您现在使用 typescript@next
,您可以尝试一下。
在你的情况下,你想要做的是
type MappedArgs = {[K in keyof TArgs]: TProps[TArgs[K]]};
type ConstructorType = new (...args: MappedArgs) => any;
但是有一些突出的问题阻止你这样做。一是由于某种原因,编译器还不理解 TArgs[K]
是 TProps
的有效索引。所以你可以引入一个 conditional type 来解决这个问题:
type Prop<T, K> = K extends keyof T ? T[K] : never;
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>};
但以下仍然无效:
type ConstructorType = new (...args: MappedArgs) => any;
// error, a rest parameter must be of an array type
嗯,MappedArgs
确实是数组类型,但 TypeScript 没有意识到这一点。也无法说服它:
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>}
& unknown[]; // definitely an array!
type ConstructorType = new (...args: MappedArgs) => any;
// error, a rest parameter must be of an array type
这似乎是映射 arrays/tuples 中的一个 outstanding bug,其中映射类型在任何地方都不会被视为数组。这很可能会在 TypeScript 3.1 的发布中得到解决。现在,您可以通过添加一个新的虚拟类型参数来解决问题,如
type Prop<T, K> = K extends keyof T ? T[K] : never;
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>}
& unknown[]; // definitely an array!
type ConstructorType<A extends MappedArgs = MappedArgs> = new (...args: A) => any;
那行得通。让我们看看我们是否可以测试这个东西。怎么样:
type Prop<T, K> = K extends keyof T ? T[K] : never;
interface NewMethod<TProps> {
create<TArgs extends Array<Extract<keyof TProps, string>>,
MTArgs extends unknown[] & { [K in keyof TArgs]: Prop<TProps, TArgs[K]> }>(
geometryClass: new (...args: MTArgs) => any,
...args: Array<Extract<keyof TProps, string>>): void;
}
declare const z: NewMethod<{ a: string, b: number }>;
z.create(null! as new (x: string, y: number) => any, "a", "b"); // okay
z.create(null! as new (x: string, y: number) => any, "a", "c"); // error, "c" is bad
z.create(null! as new (x: string, y: boolean) => any, "a", "b"); // error, constructor is bad
那些似乎按照你想要的方式行事......虽然最后一个案例中的错误真的很模糊并且似乎没有指出问题在于 y
参数的类型是boolean
并且与 TProps[keyof TProps]
中的 string
或 number
不匹配。
无论如何,截至 2018 年 8 月,这仍然是最前沿的东西,所以我认为您可能需要等待一段时间才能稳定下来,看看它究竟是如何工作的。希望有所帮助。祝你好运!
我正在尝试编写一个通用方法,它接受任意数量的作为对象键的参数,并将它们的键的值用作构造函数的参数。这是我的原始实现:
// Typescript 2.x
export function oldMethod<TProps>() {
function create<
TInstance extends Geometry | BufferGeometry,
>(
geometryClass: new () => TInstance,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1]) => TInstance,
key1: TKey1,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
TKey2 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2]) => TInstance,
key1: TKey1,
key2: TKey2,
): any;
function create<
TInstance extends Geometry | BufferGeometry,
TKey1 extends Extract<keyof TProps, string>,
TKey2 extends Extract<keyof TProps, string>,
TKey3 extends Extract<keyof TProps, string>,
>(
geometryClass: new (param1: TProps[TKey1], param2: TProps[TKey2], param3: TProps[TKey3]) => TInstance,
key1: TKey1,
key2: TKey2,
key3: TKey3,
): any;
// ...all the way up to 8 possible keys
function create<TInstance extends Geometry | BufferGeometry>(
geometryClass: new (...args: Array<TProps[Extract<keyof TProps, string>]>) => TInstance,
...args: Array<Extract<keyof TProps, string>>) {
class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
protected constructGeometry(props: TProps): TInstance {
return new geometryClass(...args.map((arg) => props[arg]));
}
}
return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
TProps,
TInstance,
GeometryContainerType> {
constructor() {
super(GeneratedGeometryWrapper, geometryClass);
this.hasRemountProps(...args);
}
};
}
return create;
}
随着 TypeScript 3.0 宣布使用元组提取和传播参数列表,我希望我能够删除重载以使其更简单:
// Typescript 3.x
export function newMethod<TProps>() {
function create<TInstance extends Geometry | BufferGeometry, TArgs extends Array<Extract<keyof TProps, string>>>(
geometryClass: new (...args: /* what goes here? */) => TInstance,
...args: Array<Extract<keyof TProps, string>>) {
class GeneratedGeometryWrapper extends GeometryWrapperBase<TProps, TInstance> {
protected constructGeometry(props: TProps): TInstance {
return new geometryClass(...args.map((arg) => props[arg]));
}
}
return class GeneratedGeometryDescriptor extends WrappedEntityDescriptor<GeneratedGeometryWrapper,
TProps,
TInstance,
GeometryContainerType> {
constructor() {
super(GeneratedGeometryWrapper, geometryClass);
this.hasRemountProps(...args);
}
};
}
return create;
}
但是,我不知道将什么作为定义构造函数类型的 args
的类型。如果我能够像 JavaScript 中的对象那样操作类型,我会这样写:...[...TArgs].map(TArg => TProps[TArg]
,但显然这不是有效的 TypeScript 语法,我想不出任何方法来做到这一点.我是否缺少表达这种类型的方法?有没有什么方法可以使它完全类型安全,而不必重载函数和有限数量的参数?是否缺少一些允许我表达这种类型的 TypeScript 功能?
我为下面的例子去掉了很多代码,但应该是一样的精神。
您缺少的功能在 2018 年 8 月的某个时候称为 mapped arrays/tuples which is planned to be released in TypeScript 3.1。您将能够像映射其他类型一样映射数组和元组,如下所示:
type Mapped<T> = {[K in keyof T]: Array<T[K]>};
type Example = Mapped<[string, number, boolean]>;
// type Example = [string[], number[], boolean[]];
如果您现在使用 typescript@next
,您可以尝试一下。
在你的情况下,你想要做的是
type MappedArgs = {[K in keyof TArgs]: TProps[TArgs[K]]};
type ConstructorType = new (...args: MappedArgs) => any;
但是有一些突出的问题阻止你这样做。一是由于某种原因,编译器还不理解 TArgs[K]
是 TProps
的有效索引。所以你可以引入一个 conditional type 来解决这个问题:
type Prop<T, K> = K extends keyof T ? T[K] : never;
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>};
但以下仍然无效:
type ConstructorType = new (...args: MappedArgs) => any;
// error, a rest parameter must be of an array type
嗯,MappedArgs
确实是数组类型,但 TypeScript 没有意识到这一点。也无法说服它:
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>}
& unknown[]; // definitely an array!
type ConstructorType = new (...args: MappedArgs) => any;
// error, a rest parameter must be of an array type
这似乎是映射 arrays/tuples 中的一个 outstanding bug,其中映射类型在任何地方都不会被视为数组。这很可能会在 TypeScript 3.1 的发布中得到解决。现在,您可以通过添加一个新的虚拟类型参数来解决问题,如
type Prop<T, K> = K extends keyof T ? T[K] : never;
type MappedArgs = {[K in keyof TArgs]: Prop<TProps,TArgs[K]>}
& unknown[]; // definitely an array!
type ConstructorType<A extends MappedArgs = MappedArgs> = new (...args: A) => any;
那行得通。让我们看看我们是否可以测试这个东西。怎么样:
type Prop<T, K> = K extends keyof T ? T[K] : never;
interface NewMethod<TProps> {
create<TArgs extends Array<Extract<keyof TProps, string>>,
MTArgs extends unknown[] & { [K in keyof TArgs]: Prop<TProps, TArgs[K]> }>(
geometryClass: new (...args: MTArgs) => any,
...args: Array<Extract<keyof TProps, string>>): void;
}
declare const z: NewMethod<{ a: string, b: number }>;
z.create(null! as new (x: string, y: number) => any, "a", "b"); // okay
z.create(null! as new (x: string, y: number) => any, "a", "c"); // error, "c" is bad
z.create(null! as new (x: string, y: boolean) => any, "a", "b"); // error, constructor is bad
那些似乎按照你想要的方式行事......虽然最后一个案例中的错误真的很模糊并且似乎没有指出问题在于 y
参数的类型是boolean
并且与 TProps[keyof TProps]
中的 string
或 number
不匹配。
无论如何,截至 2018 年 8 月,这仍然是最前沿的东西,所以我认为您可能需要等待一段时间才能稳定下来,看看它究竟是如何工作的。希望有所帮助。祝你好运!