typescripttypescript-genericsstatic-methodstype-inferenceentity-component-system

Anonymous static class infer composition no infer correctly


i get weird behavior here,

this make not sense for me !? any help are welcome

What i miss ? thanks for your time!

type Constructor<T> = new ( ...args: never ) => T;
type ComponentType<T extends Component=Component> = Constructor<T>
abstract class Component {
    declare _: never;
    declare constructorType: ComponentType<this>;
}

export type SystemWithRule<T, C extends ComponentType[]> = T extends Constructor<System<infer CC>>
    ? [ ...CC, ...C ]
    : never;

abstract class System<R extends ComponentType[]=[]> {
    static behavior( behaviors?:'update'|'dirty'|'input'|'none' ) {
        return class Compositor<CT extends ComponentType[]=[]> extends System<CT> {
            static hasAny<C extends ComponentType[], T>( this:T, components:C ) {
                return class extends Compositor< SystemWithRule<T, C> > {
                    static override hasAny:never;
                };
            }

            static hasAll<C extends ComponentType[], T>( this:T, components:C ) {
                return class extends Compositor< SystemWithRule<T, C> > {
                    static override hasAll:never;
                };
            }

            static hasMaybe<C extends ComponentType[], T>( this:T, components:C ) {
                return class extends Compositor< SystemWithRule<T, C> > {
                    static override hasMaybe:never;
                };
            }
        };
    }

    declare rules: R;
}

class ComponentA extends Component {
    declare private __: never;
}
class ComponentB extends Component {
    declare private __: never;
}

// composing systems class with custom rules: ts will AUTO infer composed rules to entities in the systems (no need duplicate interface/inplemebtation)
export class SystemA extends System
    .behavior( 'update' )
    .hasAny([
        ComponentA,
    ])

    // .hasAll([
    //  ComponentB,
    // ])
{
    update() {
        // entity should: should [typeof ComponentA | typeof ComponentB] and not  [ComponentType<Component> | typeof ComponentA | typeof ComponentB] !
        // i cant found where the [ComponentType<Component>] was infer!?, because default generic was empty array [] !!!?
        this.rules; // 🔴 
    }
}

tsplayground

V2 SIMPLIFIED: tsplayground


Solution

  • I'm not sure I follow the need for such complex recursive generic classes with static fields, but I'll consider that outside the scope of the question.

    Your problem seems to be that you except System.behavior().hasAny() to infer the default generic type argument [] for Compositor, but you're getting the constraint ComponentType[] instead. This is working as intended, as per microsoft/TypeScript#16229. Generic defaults don't play a role in inference. If you need to infer something where a generic type parameter has to be instantiated with an argument to proceed, it will end up being erased by widening to the constraint. So for a static member of a Compositor, the CT type argument will be widened to ComponentType[].

    I'm not sure the full extent of what you're doing, but if I wanted behavior like this I'd try to leverage instantiation expressions so that you can instantiate the type argument explicitly. Maybe like this:

    abstract class System<R extends ComponentType[] = []> {
        static behavior(behaviors?: string) {
            return class Compositor<CT extends ComponentType[] = []> extends System<CT> {
                static hasAny<const C extends ComponentType[], T>(this: T, components: C) {
                //            ^^^^^ const type param 
                    return class extends Compositor<SystemWithRule<T, C>> {
                        static override hasAny: never;
                    };
                }
            }<[]>; // <-- instantiation expression
        }    
        declare rules: R;
    }
    

    Here the Compositor class that's returned will automatically have its generic type argument resolved to []. Then subsequent calls should be properly generic as expected.

    Oh, I also needed to give your hasXXX() methods a const type parameter so that when you call them with an array literal like [ComponentA], the compiler interprets this as a tuple type, so that your variadic tuple type concatenations work as you expect. This is mostly off-topic, but it's an issue you would run into after solving the instantiation issue, and I didn't want to have the question digress too much.

    Let's test it:

    export class SystemA extends System.behavior('update').hasAny([ComponentA]) {
        update() {
            this.rules;
            //   ^? (property) System<[typeof ComponentA]>.rules: [typeof ComponentA]
        }
    }
    

    Looks good. The type of rules is [typeof ComponentA], as you expect.

    Playground link to code