我已经将我的方法与我最近的当前体系 struct 保持一致,努力将复杂性尽可能降至最低.

我的目标是最接近令人满意的结果,但我遇到了这种方法的一次失败的测试.

我快要放弃了,因为已经过go 三天了! 然而,如果您的专业知识能够找到一个确保所有测试成功的解决方案,那就太棒了!

getComponentFromEntity可能是关键,也许需要一个黑魔法类型的图案来解决这个问题?!

感谢您抽出宝贵的时间

enter image description here ◽playground link

//utils types
type Constructor<T> = { new(...args: any): T };
type ExtractComponentType<T> = T extends Entity<infer C> ? C : never;
type EntitiesCareMap<C extends Component> = Map<number, Entity<C>>
type ComponentType<T extends Component = Component> = Constructor<T>;
type TupleToInstances3<T extends readonly unknown[]> = {
    [K in keyof T]: T[K] extends Constructor<infer U> ? U extends {} ? U : never : never;
}

type ExtractSystemComponents4<
    S extends Rules,
    K extends RULES
> = S[K] extends ComponentType[] ? S[K] extends never[] ? UnknowComponent : TupleToInstances3<S[K]>[number] : never;

interface SystemUpdate<S extends System = System, R extends Rules = S['rules']> {
    entities: EntitiesCareMap<
        ExtractSystemComponents4<R, RULES.hasAll>
    >;

}
// issue

class Entity<
    C extends Component = Component,
> {
    declare public components: Set<C>;

    get<T extends C>(componentClass: Constructor<T>): T {
        return undefined as unknown as T;
    }
        has<T extends Component>( componentClass: Constructor<T> ): this is Entity<T> {
        return false;
    }

}
abstract class Component {
    foo() { }
}

enum RULES {
    hasAll,
}
type Rules = { readonly [K in RULES]?: ComponentType[] };
abstract class System {
    abstract rules: Rules;
    abstract onUpdate(t: SystemUpdate<System, Rules>): void;
}

export class UnknowComponent extends Component {
    #component!: never;
}

export class AComponent extends Component {
    #component!: never;
}
export class BComponent extends Component {
    #component!: never;
}
export class CComponent extends Component {
    #component!: never;
}
export class DComponent extends Component {
    #component!: never;
}

class SystemA extends System {
    public rules = {
        [RULES.hasAll]: [AComponent, BComponent],
    };

    onUpdate({entities}: SystemUpdate<SystemA>) {
        entities.forEach(( e ) => {
            e.get(BComponent)// ? this should pass.
            e.get(AComponent)// ? this should pass.
            e.get(CComponent)// ? this should error
            if (e.has(CComponent)) {
                e.get(CComponent)// ? this should pass.
                e.get(DComponent)// ? this should error
                if (e.has(DComponent)) {
                    e.get(DComponent)// ? this should pass.
                }
            }
        });
    }
}



declare const ab: Entity<BComponent> | Entity<BComponent | CComponent>;

/** Get a components from entity */
function getComponentFromEntity<E extends Entity, C extends ExtractComponentType<E>>(entity: E, component: Constructor<C>): C {
    return entity.get(component);
}

getComponentFromEntity(ab, BComponent) // ? this should pass.
getComponentFromEntity(ab, AComponent) // ? this should error.
getComponentFromEntity(ab, CComponent) // ? this should error.
//^?

declare const a: Entity<BComponent | CComponent>;
a.get(BComponent)// ? this should pass.
a.get(AComponent)// ? this should error

推荐答案

我会说,您希望ExtractComponentType<T>在输出类型中从T变为intersections,变成unions.所以ExtractComponentType<A | B>将相当于ExtractComponentType<A> & ExtractComponentType<B>.(也就是说,您希望以T的方式对并集进行distribute操作,但以contravariant的方式进行(有关差异的更多信息,请参见Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript).

这是因为当您调用getComponentFromEntity(e, c)时,如果c是类型Entity<A | B>,则c可以是AB(因为Entity<A | B>接受两者之一),但如果cEntity<A> | Entity<B>类型,那么您不知道它接受哪一个,所以c必须同时是AB才是安全的.

因此,让我们来实施它.


这里有一种方法:

type ExtractComponentType<T> =
    (T extends Entity<infer C> ? ((x: C) => void) : never) extends
    (x: infer I) => void ? I : never;

type X = ExtractComponentType<Entity<BComponent | CComponent>>;
// type X = BComponent | CComponent
type Y = ExtractComponentType<Entity<BComponent> | Entity<CComponent>>;
// type Y = BComponent & CComponent

您可以看到它按预期工作.该实现对条件类型使用逆方差技巧,如Transform union type to intersection type中所述.由于函数类型在其参数类型中是逆变量,因此我们将类型移动到函数参数位置,然后再从它进行推断.


我们已经完成了95%的目标.以下是剩下的内容:

function getComponentFromEntity<
    E extends Entity,
    C extends Component & ExtractComponentType<E>
>(entity: E, component: Constructor<C>): C {
    return entity.get(component);
}

我所要做的就是告诉编译器C肯定会是某种类型的Component,以防止实现出现问题.TS不能真正对generic个条件类型进行高阶推理,所以即使ExtractComponentType<E>必须通过构造与Component兼容,编译器也看不到这一点.所以我加了Component &来解决这个问题.


让我们来测试一下:

declare const ab: Entity<BComponent> | Entity<BComponent | CComponent>;
getComponentFromEntity(ab, BComponent) // ? okay
getComponentFromEntity(ab, AComponent) // ? error!
getComponentFromEntity(ab, CComponent) // ? error!

declare const a: Entity<BComponent | CComponent>;
a.get(BComponent)// ? okay
a.get(AComponent)// ? error!

看起来像你想要的行为!

Playground link to code

Typescript相关问答推荐

为什么ESLint抱怨通用对象类型?

TS不推断类型在条件判断后具有属性'

如果一个变量不是never类型,我如何创建编译器错误?

将值添加到具有不同类型的对象的元素

如何在TypeScript中将一个元组映射(转换)到另一个元组?

路由链接不会导航到指定router-outlet 的延迟加载模块

是否使用非显式名称隔离在对象属性上声明的接口的内部类型?

如何判断对象是否有重叠的叶子并产生有用的错误消息

使用2个泛型类型参数透明地处理函数中的联合类型

如何在方法中正确地传递来自TypeScrip对象的字段子集?

从泛型类型引用推断的文本类型

如何从输入中删除值0

是否可以通过映射类型将函数参数约束为预定义类型?

将布尔值附加到每个对象特性

在REACT查询中获取未定义的isLoading态

记录的子类型<;字符串,X>;没有索引签名

如何根据Typescript 中带有泛型的对象嵌套键数组获取数组或对象的正确类型?

对象只能使用 Typescript 中另一个对象的键

当受其他参数限制时,通用参数类型不会缩小

将 Angular 从 14 升级到 16 后出现环境变量注入问题