英文:
TypeScript: accept an array of different generics and return a merged type
问题
我试图弄清楚实体组件系统,为了提高可用性,我需要能够传递不同组件定义的数组并获得合并类型作为结果。
我试图做到这一点:
const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });
world.withComponents(Position, Container).forEach(([id, entity]) => {
console.log(entity.x);
console.log(entity.capacity);
});
完整代码:
interface ISchema {}
type SimpleType = number | string | boolean;
type Type<V> = V extends SimpleType
? V
: V extends Array<infer RT>
? Array<Type<RT>>
: V extends ISchema
? ComponentType<V>
: never;
type ComponentType<T extends ISchema> = {
[key in keyof T]: Type<T[key]>;
};
type ComponentDefinition<T extends ISchema> = {
id: Symbol;
type: ComponentType<T>;
};
function defineComponent<T extends ISchema>(schema: T): ComponentDefinition<T> {
return {
id: Symbol(),
type: schema as ComponentType<T>,
};
}
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void;
}[number] extends (x: infer I) => void
? I
: never;
class World {
protected readonly entities = new Map<number, { data: ISchema; components: Set<Symbol> }>();
withComponents<T extends ComponentType<ISchema>>(
definitions: ComponentDefinition<T>[]
): Array<[number, TupleToIntersection<T[]>]> {
const result: Array<[number, TupleToIntersection<T[]>]> = [];
for (const [id, entity] of this.entities) {
let isMatch = true;
for (const def of definitions) {
if (!entity.components.has(def.id)) {
isMatch = false;
break;
}
}
if (isMatch) {
result.push([id, entity.data as TupleToIntersection<T[]>]);
}
}
return result;
}
}
目前,我在这一行上遇到以下错误:
world.withComponents([Position, Container])
类型 'ComponentDefinition<{ capacity: number; contents: { item: number; amount: number; }[]; }>' 的参数不能赋给类型 'ComponentDefinition<{ x: number; y: number; }>' 的参数。
类型 'ComponentType<{ capacity: number; contents: { item: number; amount: number; }[]; }>' 中缺少属性 'x'、'y' 的类型。
所以我的问题是我无法弄清楚如何接受不同泛型的数组。
英文:
I'm trying to figure out Entity Component System and for usability I need to be able to pass an array of different Component definitions and get a merged type as a result.
I'm trying to do this:
const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });
world.withComponents(Position, Container).forEach(([id, entity]) => {
console.log(entity.x);
consone.log(entity.capacity);
});
Full code:
interface ISchema {}
type SimpleType = number | string | boolean;
type Type<V> = V extends SimpleType
? V
: V extends Array<infer RT>
? Array<Type<RT>>
: V extends ISchema
? ComponentType<V>
: never;
type ComponentType<T extends ISchema> = {
[key in keyof T]: Type<T[key]>;
};
type ComponentDefinition<T extends ISchema> = {
id: Symbol;
type: ComponentType<T>;
};
function defineComponent<T extends ISchema>(schema: T): ComponentDefinition<T> {
return {
id: Symbol(),
type: schema as ComponentType<T>,
};
}
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void;
}[number] extends (x: infer I) => void
? I
: never;
class World
{
protected readonly entities = new Map<number, { data: ISchema; components: Set<Symbol> }>();
withComponents<T extends ComponentType<ISchema>>(
definitions: ComponentDefinition<T>[]
): Array<[number, TupleToIntersection<T[]>]> {
const result: Array<[number, TupleToIntersection<T[]>]> = [];
for (const [id, entity] of this.entities) {
let isMatch = true;
for (const def of definitions) {
if (!entity.components.has(def.id)) {
isMatch = false;
break;
}
}
if (isMatch) {
result.push([id, entity.data as TupleToIntersection<T[]>]);
}
}
return result;
}
}
Currently I getting the following error on this line:
world.withComponents([Position, Container])
Argument of type 'ComponentDefinition<{ capacity: number; contents: { item: number; amount: number; }[]; }>' is not assignable to parameter of type 'ComponentDefinition<{ x: number; y: number; }>'.
Types of property 'type' are incompatible.
Type 'ComponentType<{ capacity: number; contents: { item: number; amount: number; }[]; }>' is missing the following properties from type 'ComponentType<{ x: number; y: number; }>': x, yts(2345)
So my problem is that I'm not able to figure out how to accept an array of different generics.
答案1
得分: 1
以下是代码部分的翻译:
You can make withComponents() generic in the tuple type T of arguments to ComponentDefinition<> for each element of the definitions input. That is, if definitions is of type [ComponentDefinition<X>, ComponentDefinition<Y>, ComponentDefinition<Z>], then T would be [X, Y, Z]. Then you can make the type of definitions be a mapped type over the tuple T, and the compiler can infer T from definitions because it's a homomorphic mapped type (see https://stackoverflow.com/q/59790508/2887218). Like this:
将withComponents() 泛型 应用到 ComponentDefinition<> 的参数元组类型 T 中,用于处理 definitions 输入的每个元素。也就是说,如果 definitions 的类型是 [ComponentDefinition<X>, ComponentDefinition<Y>, ComponentDefinition<Z>],那么 T 就会是 [X, Y, Z]。然后,可以将 definitions 的类型设定为元组 T 上的映射类型,编译器可以从 definitions 推断出 T,因为它是同态映射类型(参见 https://stackoverflow.com/q/59790508/2887218)。就像这样:
withComponents<T extends ComponentType<ISchema>[]>(
definitions: [...{ [I in keyof T]: ComponentDefinition<T[I]> }]
): Array<[number, TupleToIntersection<T>]>
So { [I in keyof T]: ComponentDefinition<T[I]> } is the relevant mapped type. And I've wrapped it in a variadic tuple type [...+], which, as the implementing pull request microsoft/TypeScript#39094 mentions, "can conveniently be used to indicate a preference for inference of tuple types".
所以 { [I in keyof T]: ComponentDefinition<T[I]> } 是相关的映射类型。而且我已将它包裹在一个变参元组类型 [...+] 中,正如实现的 pull request microsoft/TypeScript#39094 中所提到的,它可以方便地用于指示对元组类型的推断偏好。
And everywhere else in your code that uses T will need to be updated to account for the fact that it's [X, Y, Z] and not X | Y | Z... that is, every TupleToIntersection<T[]> should be replaced with TupleToIntersection<T>.
而在你的代码的其他地方使用 T 的地方都需要更新,以考虑它是 [X, Y, Z] 而不是 X | Y | Z...也就是说,每个 TupleToIntersection<T[]> 都应该替换为 TupleToIntersection<T>。
Let's try it out:
让我们试一试:
const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });
const world = new World();
world.withComponents([Position, Container]).forEach(([id, entity]) => {
console.log(entity.x);
console.log(entity.capacity);
});
Looks good!
看起来不错!
英文:
You can make withComponents() generic in the tuple type T of arguments to ComponentDefinition<> for each element of the definitions input. That is, if definitions is of type [ComponentDefinition<X>, ComponentDefinition<Y>, ComponentDefinition<Z>], then T would be [X, Y, Z]. Then you can make the type of definitions be a mapped type over the tuple T, and the compiler can infer T from definitions because it's a homomorphic mapped type (see https://stackoverflow.com/q/59790508/2887218). Like this:
withComponents<T extends ComponentType<ISchema>[]>(
definitions: [...{ [I in keyof T]: ComponentDefinition<T[I]> }]
): Array<[number, TupleToIntersection<T>]>
So { [I in keyof T]: ComponentDefinition<T[I]> } is the relevant mapped type. And I've wrapped it in a variadic tuple type [...+], which, as the implementing pull request microsoft/TypeScript#39094 mentions, "can conveniently be used to indicate a preference for inference of tuple types".
And everywhere else in your code that uses T will need to be updated to account for the fact that it's [X, Y, Z] and not X | Y | Z... that is, every TupleToIntersection<T[]> should be replaced with TupleToIntersection<T>.
Let's try it out:
const Position = defineComponent({ x: 0, y: 0 });
const Container = defineComponent({ capacity: 0, contents: new Array<{ item: number; amount: number }>() });
const world = new World();
world.withComponents([Position, Container]).forEach(([id, entity]) => {
console.log(entity.x);
console.log(entity.capacity);
});
Looks good!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论