Conditional types with Generics in TypeScript

萝らか妹 提交于 2020-05-29 07:26:55

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!