TypeScript function return type based on input parameter

Typescript

Typescript Problem Overview


I have a few different interfaces and objects that each have a type property. Let's say these are objects stored in a NoSQL db. How can I create a generic getItem function with a deterministic return type based on its input parameter type?

interface Circle {
    type: "circle";
    radius: number;
}

interface Square {
    type: "square";
    length: number;
}

const shapes: (Circle | Square)[] = [
    { type: "circle", radius: 1 },
    { type: "circle", radius: 2 },
    { type: "square", length: 10 }];

function getItems(type: "circle" | "square") {
    return shapes.filter(s => s.type == type);
    // Think of this as items coming from a database
    // I'd like the return type of this function to be
    // deterministic based on the `type` value provided as a parameter. 
}

const circles = getItems("circle");
for (const circle of circles) {
    console.log(circle.radius);
                       ^^^^^^
}

> Property 'radius' does not exist on type 'Circle | Square'.

Typescript Solutions


Solution 1 - Typescript

Conditional Types to the rescue:

interface Circle {
    type: "circle";
    radius: number;
}

interface Square {
    type: "square";
    length: number;
}

type TypeName = "circle" | "square"; 

type ObjectType<T> = 
    T extends "circle" ? Circle :
    T extends "square" ? Square :
    never;

const shapes: (Circle | Square)[] = [
    { type: "circle", radius: 1 },
    { type: "circle", radius: 2 },
    { type: "square", length: 10 }];

function getItems<T extends TypeName>(type: T) : ObjectType<T>[]  {
    return shapes.filter(s => s.type == type) as ObjectType<T>[];
}

const circles = getItems("circle");
for (const circle of circles) {
    console.log(circle.radius);
}

Thanks Silvio for pointing me in the right direction.

Solution 2 - Typescript

You're looking for overload signatures

function getItems(type: "circle"): Circle[]
function getItems(type: "square"): Square[]
function getItems(type: "circle" | "square") {
    return shapes.filter(s => s.type == type);
}

Putting multiple type signatures before the actual definition allows you to list different "cases" into which your function's signature can fall.

Edit after your comment

So it turns out, what you're wanting is possible, but we may have to jump through a few hoops to get there.

First, we're going to need a way to translate each name. We want "circle" to map to Circle, "square" to Square, etc. To this end, we can use a conditional type.

type ObjectType<T> =
  T extends "circle" ? Circle :
  T extends "square" ? Square :
  never;

(I use never as the fallback in the hopes that it very quickly creates a type error if you somehow end up with an invalid type)

Now, I don't know of a way to parameterize over the type of a function call like you're asking for, but Typescript does support parameterizing over the keys of an object by means of mapped typed. So if you're willing to trade in the getItems("circle") syntax for getItems["circle"], we can at least describe the type.

interface Keys {
  circle: "circle";
  square: "square";
}

type GetItemsType = {
  [K in keyof Keys]: ObjectType<K>[];
}

Problem is, we have to actually construct an object of this type now. Provided you're targeting ES2015 (--target es2015 or newer when compiling), you can use the Javascript Proxy type. Now, unfortunately, I don't know of a good way to convince Typescript that what we're doing is okay, so a quick cast through any will quell its concerns.

let getItems: GetItemsType = <any>new Proxy({}, {
  get: function(target, type) {
    return shapes.filter(s => s.type == type);
  }
});

So you lose type checking on the actual getItems "function", but you gain stronger type checking at the call site. Then, to make the call,

const circles = getItems["circle"];
for (const circle of circles) {
    console.log(circle.radius);
}

Is this worth it? That's up to you. It's a lot of extra syntax, and your users have to use the [] notation, but it gets the result you want.

Solution 3 - Typescript

I ran into a similar issue. If you don't want to have both TypeName and ObjectType types, this can also be done using a single interface:

interface TypeMap {
  "circle": Circle;
  "square": Square;
}

function getItems<T extends keyof TypeMap>(type: T) : TypeMap[T][]  {
  return shapes.filter(s => s.type == type) as TypeMap[T][];
}

Solution 4 - Typescript

As you have mentioned that data is comming from No-Sql database with a type property. you can create type property as string value and change your interfaces as a class to check instanceOf in your function.

class Circle {
    type: string;
    radius: number;
}

class Square {
    type: string;
    length: number;
}

const shapes: (Circle | Square)[] = [
    { type: "circle", radius: 1 },
    { type: "circle", radius: 2 },
    { type: "square", length: 10 }];

function getItems(type: string) {
    return shapes.filter(s => s.type == type);
    // Think of this as items coming from a database
    // I'd like the return type of this function to be
    // deterministic based on the `type` value provided as a parameter. 
}

const circles = getItems("circle");
for (const circle of circles) {
    if (circle instanceof Circle) {
        console.log(circle.radius);
    } else if (circle instanceof Square) {
        console.log(circle.length);
    }
} 

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionArash MotamediView Question on Stackoverflow
Solution 1 - TypescriptArash MotamediView Answer on Stackoverflow
Solution 2 - TypescriptSilvio MayoloView Answer on Stackoverflow
Solution 3 - TypescriptJonathan SudiamanView Answer on Stackoverflow
Solution 4 - Typescripter_vaibhsView Answer on Stackoverflow