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
//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

I'd say that you want
ExtractComponentType<T>to turn unions inTto intersections in the output type. SoExtractComponentType<A | B>will be equivalent toExtractComponentType<A> & ExtractComponentType<B>. (That is, you want to distribute your operation over unions inTbut 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), ifcis of typeEntity<A | B>thenccan be eitherAorB(becauseEntity<A | B>accepts either), but ifcis of typeEntity<A> | Entity<B>then you don't know which it accepts, sochas to be bothAandBfor that to be safe.So let's implement it.
Here's one way:
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:
All I had to do there is tell the compiler that
Cwould definitely be aComponentof some sort, to prevent the implementation from complaining. TS can't really do higher order reasoning about generic conditional types, so even thoughExtractComponentType<E>must be compatible withComponentby construction, the compiler fails to see it. So I addedComponent &to fix that.Let's test it:
Looks like the behavior you wanted!
Playground link to code