typescripttypescript-genericsentity-component-system

branding with type predicates in intersections


I have a small problem that I've been unable to solve after several hours. I don't know if a sharper mind could offer me a solution?

But unfortunately, i can't seem to find a solution to allow this approach. Maybe a lacking from typescript !?

Everything work as i want, but this thing drive me crazy :) ! If anyone has any ideas ho to remove this last error in my code ? see: 🔴

I've simplified the example to the minimum

thanks a lot

type RemoveSuper<A extends number[]> = { // remove number constructor in tuple [number,1,2] => [1,2]
    [K in keyof A]: number extends A[K] ? never : A[K]
}

type EntityWith<N extends number[] > = { //entity with numbers (branding) 
    has< T extends N >( componentType: T ):boolean
}

type ExtractGenericInIntersection<T> = (
    T extends Entity<infer C> ? ( c:C[number]) => void : never
) extends ( r: infer R2 ) => void ? [R2[]] : never

type Entities<T extends Entity> = ExtractGenericInIntersection<T> extends [infer R extends number[]]
    ? EntityWith<R> | Entity<R>
    : never

export interface Entity<C extends number[] = number[]> {
    has: <T extends RemoveSuper<C>[number]>( componentType: T ) => this is Entity<[T]>;
}
type A = Entity<[1]>
type B = Entity<[1, 2]>

declare const entities: Entities<A|B>;

//@ts-expect-error
entities.has( 2 );

entities.has( 1 );//🔴

tiny version

short version

long version


Solution

  • The problem was related to a limitation of type that do not handle this.

    Interfaces seem to support this, which allows branding works.

    By replacing type with interface the branding work.

    type EntityWith<
        C extends Component,
        R extends ComponentType = never,
        E extends Entity = Entity,
    > = E & {
        has<T extends R>(componentType: T): boolean
    }
    

    by

    interface EntityWith<
        C extends Component,
        R extends ComponentType = never,
    > extends Entity {
        has< T extends R>( componentType: T ): this is Entity<InstanceType<T>, []>
    }
    

    Now it reflet correcly the method in class

    abstract class Entity {
        declare has: <T extends RemoveSuper<M>>(componentType: T) => this is Entity<InstanceType<T>, []>
    }