gerrod
gerrod

Reputation: 6627

How to specify return type of a promise in typescript based on the input type

I'm trying to create a "rollup" service in TypeScript that can save an object based on it's type, but TypeScript is telling me I'm doing something illegal (TS2322).

Here's a simplified version of my code; first I have an enum which describes all the object classes that I have:

enum ThingType {
    SomeThing = 'SomeThing',
    OtherThing = 'OtherThing',
}

Then there's an interface that describes the base type, as well as a couple of derived types:

interface IThing {
    name: string;
    type: ThingType;
}

interface ISomeThing extends IThing {
    // properties of ISomeThing
}

interface IOtherThing extends IThing {
    // properties of IOtherThing
}

For each derived type, there's a specific service that saves objects of those types:

function saveSomeThing(someThing: ISomeThing): Promise<ISomeThing> {
    return Promise.resolve(someThing);
}

function saveOtherThing(otherThing: IOtherThing): Promise<IOtherThing> {
    return Promise.resolve(otherThing);
}

The problem

Now I want to have a single "rollup" service that delegates the save operation to the correct implementation:

function saveThing<T extends IThing>(thing: T): Promise<T> {
    switch (thing.type) {
        case ThingType.SomeThing: return saveSomeThing(thing as ISomeThing);
        case ThingType.OtherThing: return saveOtherThing(thing as IOtherThing);
        default: throw new Error('Unknown type: ' + thing.type);
    }
}

TypeScript tells me that my return statements are invalid:

TS2322: Type 'Promise<ISomeThing>' is not assignable to type 'Promise<T>'.
   Type 'ISomeThing' is not assignable to type 'T'.
     'ISomeThing' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'IThing'.

That's fair enough - TypeScript doesn't know that T will be an ISomeThing for that branch of the switch - is there a way I can tell TypeScript that it is (without resorting to an unknown or any)?

Upvotes: 1

Views: 4404

Answers (1)

Elias Schablowski
Elias Schablowski

Reputation: 2812

There are two ways:

Marking your return with as any(you don't lose any type information since the function itself is typed)

Mostly here since the other way is rather cumbersome and does not add any real value:

function saveThing<T extends IThing>(thing: T): Promise<T> {
    switch (thing.type) {
        case ThingType.SomeThing: return saveSomeThing(thing as ISomeThing) as any;
        case ThingType.OtherThing: return saveOtherThing(thing as IOtherThing) as any;
        default: throw new Error('Unknown type: ' + thing.type);
    }
}

Playground

Using a type guard function and a conditional return type

function saveThing<T extends IThing>(thing: T): Promise<T extends ISomeThing ? ISomeThing : IOtherThing> {
    type returnType = T extends ISomeThing ? ISomeThing : IOtherThing;
    if(isISomeThing(thing)){
        return saveSomeThing(thing) as Promise<returnType>;
    }
    if(isIOtherThing(thing)){
        return saveOtherThing(thing) as Promise<returnType>;
    }

    throw new Error('Unknown type: ' + thing.type);

    function isISomeThing(potentialISomeThing: IThing): potentialISomeThing is ISomeThing {
        return potentialISomeThing.type === ThingType.SomeThing;
    }
    function isIOtherThing(potentialIOtherThing: IThing): potentialIOtherThing is IOtherThing {
        return potentialIOtherThing.type === ThingType.OtherThing;
    }
}

Playground

Upvotes: 3

Related Questions