typescriptgenericsnested-generics

Nested Typescript Generics


I have the following classes:

class Walls { }
class Furniture { }
class Layout<T extends Walls | Furniture> { }
class Space extends Layout<Walls> { }
class Room extends Layout<Furniture> { }

I need to create these two classes:

class SpaceController extends LayoutController<Space> { }
class RoomController extends LayoutController<Room> {}

To do this, I can't create LayoutController class like this:

class LayoutController<T extends Layout>{ }

because Layout needs a Type parameter.

I can instead create this:

class LayoutController<U, T extends Layout<U extends Walls | Furniture>>{ }

but that would mean I will have to do this:

class SpaceController extends LayoutController<Walls, Space> { }
class RoomController extends LayoutController<Furniture, Room> {}

which I feel is redundant. Moreover, it opens up room for errors. There's nothing stopping me from writing:

class RoomController extends LayoutController<Walls, Room> {}

How do I solve this?

More details about LayoutController:

class LayoutController<T> extends React.Component<{}, LayoutControllerState<T>>() { }
interface LayoutControllerState<T> { 
  selectedLayout: T;
}

Solution

  • While a bit more typing a two type parameter solution is not bad and will give you appropriate errors if U is not compatible with the T expected by layout, if type constraints are properly specified:

    class Walls { height!: number; }
    class Furniture { price!: number; }
    class Layout<T extends Walls | Furniture> { children: T[] = []; }
    class Space extends Layout<Walls> { private x: undefined; }
    class Room extends Layout<Furniture> { private x: undefined; }
    
    class LayoutController<U extends Walls | Furniture, T extends Layout<U>>{
        getValue(u: U) : void{}
    }
    
    class SpaceController extends LayoutController<Walls, Space> { }
    class RoomController extends LayoutController<Furniture, Room> {}
    class ErrController extends LayoutController<Walls, Room> {}  //Type 'Room' does not satisfy the constraint 'Layout<Walls>
    

    We can use a conditional type to extract the generic parameter from the Layout type and provide this as default for U. Thus we don't have to specify the redundant parameter:

    type ExtractLayoutParameter<T extends Layout<any>> = T extends Layout<infer U> ? U: never;
    class LayoutController<T extends Layout<any>, U extends Walls | Furniture= ExtractLayoutParameter<T>>{
        getValue(u: U) : void{}
    }
    
    class SpaceController extends LayoutController<Space> { }
    class RoomController extends LayoutController<Room> {}
    new SpaceController().getValue(new Walls())
    new SpaceController().getValue(new Furniture()) // error
    

    We could also use the conditional type instead of U thus not allowing the user to change U to a derived type of that accepted by the layout (depeding on your use case a feature or a design limitation you decide):

    type ExtractLayoutParameter<T extends Layout<any>> = T extends Layout<infer U> ? U: never;
    class LayoutController<T extends Layout<any>>{
        getValue(u: ExtractLayoutParameter<T>) : void{}
    }
    
    class SpaceController extends LayoutController<Space> { }
    class RoomController extends LayoutController<Room> {}
    new SpaceController().getValue(new Walls())
    new SpaceController().getValue(new Furniture()) // error