23
votes

Can I have variable return type based on the value of a string literal type argument in TypeScript 1.8 or 2.0?

type Fruit = "apple" | "orange" 
function doSomething(foo : Fruit) : string | string[] {
    if (foo == "apple") return "hello";
    else return ["hello","world"];
}

var test : string[] = doSomething("orange");

Error: TS2322: Type 'string | string[]' is not assignable to type 'string[]'.

3
test variable type should be string[]|stringAleksey L.
@AlekseyL. I disagree. If passing "orange" as the argument to doSomething always yields a string[], then it is correct for test to have that type as well. You need to use overloading.John Weisz
@JohnWhite doSomething signature clearly states that the return type is string[]|string which is not the case with overloading where you just specifying return type according to specific inputAleksey L.
@AlekseyL. I guess it's really a matter of pespective. However, "orange" => string[] is the signature that actually represents the runtime behaviour, and not "orange" => string | string[].John Weisz
You could use conditional types, see this answer: stackoverflow.com/a/55059318/2684980Jørgen Tvedt

3 Answers

42
votes

Yes, you can use overload signatures to achieve exactly what you want:

type Fruit = "apple" | "orange"

function doSomething(foo: "apple"): string;
function doSomething(foo: "orange"): string[];
function doSomething(foo: Fruit): string | string[]
{
    if (foo == "apple") return "hello";
    else return ["hello", "world"];
}

let orange: string[] = doSomething("orange");
let apple: string = doSomething("apple");

Trying to assign doSomething("apple") to orange would yield a compile-time type-error:

let orange: string[] = doSomething("apple");
 // ^^^^^^
 // type 'string' is not assignable to type 'string[]'

Live demo on TypeScript Playground

It is important to note that determining which overload signature was used must always be done in the function implementation manually, and the function implementation must support all overload signatures.

There are no separate implementations per overload in TypeScript as there are in, say, C#. As such, I find it a good practice to reinforce TypeScript type-checks at runtime, for example:

switch (foo) {
    case "apple":
        return "hello";
    case "orange":
        return ["hello", "world"];
    default:
        throw new TypeError("Invalid string value.");
}
4
votes

I have a better approach. use a generic which is then used as the type of the argument (So then you won't need to pass the generic manually and typescript will infer it automatically). Then you can use that type and choose the correct return type.

type Fruit = 'apple' | 'orange';
function doSomething<P extends Fruit>(foo: P): ({ apple: string; orange: string[] })[P] {
  if (foo === 'apple') return 'hello';
  return ['hello','world];
}
const x: string = doSomething('apple');
const y: string[] = doSomething('orange');

This way you can change the return type of your function based on the argument passed automatically.

0
votes

Yes you can. You just need to test your test variable with instanceof. Typescript will then limit the type.

type Fruit = "apple" | "orange" 
function doSomething(foo: Fruit): string | string[] {
    if (foo == "apple") return "hello";
    else return ["hello","world"]
}

// here the type is still inferred as: string | string[]
var test = doSomething("orange");

if (test instanceof String) {
    // TypeScript knows test is type: string
    doSomethingWithString(test);
} else {
    // TypeScript knows test is type: string[]
    doSomethingWithStringArray(test);
}

function doSomethingWithString(input: string) {}
function doSomethingWithStringArray(input: string[]) {}

UPDATE

You may just want to make the method generic instead.

function doSomething<T>(foo: Fruit): T {
    if (foo == "apple") return "hello";
    else return ["hello","world"]
}

var test1 = doSomething<string>("apple");
var test2 = doSomething<string[]>("orange");

Or another option would be to invert the flow to something like this:

type Fruit = "apple" | "orange" 
function doSomething(foo: Fruit): void {
    if (foo == "apple") 
        doSomthingWithString("hello");
    else 
        doSomethingWithStringArray(["hello","world"]);
}

function doSomethingWithString(input: string) {}
function doSomethingWithStringArray(input: string[]) {}

UPDATE

Actually I believe John White's is a much better answer.