19

What I want to achieve can be best explained in code:

Given shoe and dress class:

class Shoe { constructor(public size: number){} } 
class Dress { constructor(public style: string){} } 

Have a generic box that can only contain Shoe or Dress. Can't contain both:

class Box <T extends Shoe | Dress > { } 

Then have a utility class that takes care of moving shoes:

class ShoeMover { constructor(public size: number[]){} } 

Also, a utility class for moving Dresses:

class DressPacker { constructor(public style: string[]){} } 

Then have a generic mover, that if instantiated with Box<Shoe> or Box<Dress> has a mover method that makes use of either the ShoeMover or the DressPacker:

class Move<B extends Box<Shoe> | Box<Dress>> { private box: B; constructor(toMove: B) { this.box = toMove; } public mover(tool: ShoeMover | DressPacker) { } } 

Then the compile time guarantee should be that, if Move is instantiated with Box<Shoe>, then the mover method should only accept ShoeMover. If instantiated with Box<Dress>. the mover method should only accept DressPacker. That is:

let shoemover = new Move(new Box<Shoe>()); // compile shoemover.mover(new ShoeMover([21])) // should not compile. But currently does shoemover.mover(new DressPacker(["1"])) 

I tried using conditional types, but I guess the fact that generics is involved makes the intended solution not to work. Basically this is what I have tried:

type MoverFromEitherShoeOrDressA<T> = T extends Box<infer U> ? U extends Shoe ? ShoeMover : U extends Dress ? DressPacker : never: never; and type MoverFromEitherShoeOrDressB<T> = T extends Box<Shoe> ? ShoeMover: T extends Box<Dress> ? DressPacker: never; 

Then changing the definition of mover from:

public mover(tool: ShoeMover | DressPacker) { } 

to

public mover(tool: MoverFromEitherShoeOrDressB) { } or public mover(tool: MoverFromEitherShoeOrDressA) { } 

..but these did not give the compile time guarantees that I sought.

Anyone knows how to achieve this?

Edit.

The accepted answer works for the scenario above. But there is a slightly different scenario which does not work. Instead of creating another question, I decided to update. The scenario is when the constructor of the Move is changed to take in union type.

type Mover<T> = T extends Shoe ? ShoeMover : T extends Dress ? DressPacker : never; class Move<T extends Shoe | Dress> { private box: Box<T>; constructor(public toMove: Box<Shoe>[] | Box<Dress>[]) { this.box = toMove; } public mover(tool: Mover<T>) { } } let shoemover = new Move(new Array<Box<Shoe>>()); // compile shoemover.mover(new ShoeMover([21])) // should not compile. But currently does shoemover.mover(new DressPacker(["1"])) 

Playground Link

1 Answer 1

18

You were almost there, you just need to use generics in the mover method too, elseway it will not know what T is is. See the generic type as a method which takes a generic T as a parameter, and <> as ():

type Mover<T> = T extends Shoe ? ShoeMover : T extends Dress ? DressPacker : never; class Move<T extends Shoe | Dress> { private box: Box<T>; constructor(toMove: Box<T>) { this.box = toMove; } public mover(tool: Mover<T>) { } } 

Furthermore, I changed the Move definition to exclude the Box generic since you can easily encapsulate it in the class inner definitions, but your solution would work too with:

type MoverFromEitherShoeOrDressA<T> = T extends Box<infer U> ? U extends Shoe ? ShoeMover : U extends Dress ? DressPacker : never: never; public mover(tool: MoverFromEitherShoeOrDressA<B>) { // <-- Here } 

Edit: added playground here: Playground Link

Sign up to request clarification or add additional context in comments.

2 Comments

As always I was not patient enough to reproduce the exact problem I have. The exact situation involves see move constructor being like this: constructor(public toMove: Box<Shoe>[] | Box<Dress>[]) // because move can move multiple boxes of Box of shoes or Box of dress.
@FinlayWeber add addition information to your question, not to the answer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.