Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript function return type based on input parameter

Tags:

typescript

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'.

like image 470
Arash Motamedi Avatar asked Jan 13 '19 02:01

Arash Motamedi


People also ask

Can a function return a type TypeScript?

We can return any type of value from the function or nothing at all from the function in TypeScript. Some of the return types is a string, number, object or any, etc. If we do not return the expected value from the function, then we will have an error and exception.

Can you pass a type as a parameter TypeScript?

This is not possible.

What does () => void mean TypeScript?

Introduction to TypeScript void type The void type denotes the absence of having any type at all. It is a little like the opposite of the any type. Typically, you use the void type as the return type of functions that do not return a value.

How do you call a function with parameters in TypeScript?

In functions, parameters are the values or arguments that passed to a function. The TypeScript, compiler accepts the same number and type of arguments as defined in the function signature. If the compiler does not match the same parameter as in the function signature, then it will give the compilation error.

How to return a value from a function in typescript?

3. : return_type: This is the standard given by the TypeScript documentation to define the return type in TypeScript. We have to use the ‘:’ colon symbol to make this function return any value from it. Immediately after this, we can specify the type that we want to return from our function; it can be anything like string, number, or any, etc.

Why do we use ‘any’ type in typescript?

One advantage of using ‘any’ type in TypeScript is that we can return anything from our function then. By using that, our function is not specific to return number or string; rather, we can return anything from it. Now we will see one sample example for beginners to understand its implementation and usage see below;

Can typescript infer the return type of the longest type?

We allowed TypeScript to infer the return type of longest . Return type inference also works on generic functions. Because we constrained Type to { length: number }, we were allowed to access the .length property of the a and b parameters.

How do I use generics in typescript?

In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature: By adding a type parameter Type to this function and using it in two places, we’ve created a link between the input of the function (the array) and the output (the return value).


3 Answers

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.

like image 173
Arash Motamedi Avatar answered Oct 19 '22 21:10

Arash Motamedi


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.

like image 24
Silvio Mayolo Avatar answered Oct 19 '22 21:10

Silvio Mayolo


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][];
}
like image 9
Jonathan Sudiaman Avatar answered Oct 19 '22 19:10

Jonathan Sudiaman