1

I have aligned my approach with my nearest current architecture, striving to minimize complexity as much as possible.

I have aimed for the closest approximation to a satisfactory result, but I encountered a single failed test with this approach.

I'm on the verge of giving up, as it has already been three days! However, if your expertise can uncover a solution that ensures the success of all tests, it would be truly amazing!

the getComponentFromEntity is maybe the key, maybe need a black magic type pattern to solve this ?!

Thank you for your valuable time

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
jon
  • 1,494
  • 3
  • 16
  • 29
  • 1
  • in fake my fist issue was this : https://tsplay.dev/WGX69w i gave up on this one to the new approach because i think it was impossible due to the architecture, but maybe with luck you have a magic solution! – jon Aug 13 '23 at 03:50
  • and +1 for explanation when you will have 5min plz , i dont get your black magic thank you so mutch for your time – jon Aug 13 '23 at 03:55
  • I have to go to sleep but I'll write an answer when I wake up. I'm afraid I don't understand [this comment](https://stackoverflow.com/questions/76891793/this-expression-is-not-callable-each-members-of-the-union#comment135552305_76891793) much. Are you saying you need a different solution to a different version of the question? I plan to answer the question that you actually asked and not some other version from the comments; I hope that's okay. – jcalz Aug 13 '23 at 04:32
  • I've written very poorly, Sorry for my bad English. In fact, it was the first and original version of the issue that I abandoned after trying in all directions to make the polymorphisme and unions of the 2 entities work. The diff is just Instead to call a function, we use local method and polymorphisme. But ts throw error with generic. I had concluded that it was probably impossible after many try, but I thought that maybe someone more experienced might have a solution to unlock this case. Otherwise, it's not a big deal, the solution above you solved is more than suitable for me! – jon Aug 13 '23 at 05:07

1 Answers1

1

I'd say that you want ExtractComponentType<T> to turn unions in T to intersections in the output type. So ExtractComponentType<A | B> will be equivalent to ExtractComponentType<A> & ExtractComponentType<B>. (That is, you want to distribute your operation over unions in T but in a contravariant way (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more info on variance).

That's because when you call getComponentFromEntity(e, c), if c is of type Entity<A | B> then c can be either A or B (because Entity<A | B> accepts either), but if c is of type Entity<A> | Entity<B> then you don't know which it accepts, so c has to be both A and B for that to be safe.

So let's implement it.


Here's one way:

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

You can see that it works as intended. The implementation uses a contravariance trick with conditional types, as described in Transform union type to intersection type. Since function types are contravariant in their parameter types, we move the type into a function parameter position before inferring from it.


We're 95% of the way there. Here's the rest:

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

All I had to do there is tell the compiler that C would definitely be a Component of some sort, to prevent the implementation from complaining. TS can't really do higher order reasoning about generic conditional types, so even though ExtractComponentType<E> must be compatible with Component by construction, the compiler fails to see it. So I added Component & to fix that.


Let's test it:

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!

Looks like the behavior you wanted!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • thank you so much for your time and the explantations, I will definitely have to deepen my research on Variance, Covariance, Contravariance and Covariance. These terms were unfamiliar to me and the contravariance trick is hard to digest at the moment. I also close the discord discutions and link here for more context. https://discord.com/channels/508357248330760243/1139987469144694876 – jon Aug 13 '23 at 17:51