问题
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:
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
来源:https://stackoverflow.com/questions/61541059/conditional-types-with-generics-in-typescript