2

I wanted to use the overload feature of TypeScript to create a function that returns different types based on the arguments passed in. I manage to get it work but the compiler is not able to catch an error inside the implementation of the overloaded function.

The example below is the one from the TypeScript documentation (see below). The function accepts two different types for its arguments:

  • object: expect to return the type number
  • number: expect to return the type object

// With incompatible types

const suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    // This part does not match the overload definition. The signature
    // expect a `number` but we provide a `string`. The compiler does
    // not throw an error in that case.
    return pickedCard.toString();
  } else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

const card = pickCard([{ suit: 'hearts', card: 5 }])

card.toFixed() // throw a runtime error: card.toFixed is not a function

// With compatible types

type Hand = { suit: string; card: number };

type HandWithScore = Hand & { score: number }

const suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: Hand[]): HandWithScore;
function pickCard(x: number): Hand;
function pickCard(x): HandWithScore | Hand {
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    // This part does not match the overload definition.
    return { suit: 'hearts', card: x % 13 };
  } else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

const card = pickCard([{ suit: 'hearts', card: 5 }])

card.score.toFixed() // throw a runtime

The implementation does not match the definition of the overload and the compiler does not warn us in that case, which means that we might have issue at the runtime since we expect a number but we actually get a string. Is it expected that the compiler does not throw?

You can test the sample inside the TypeScript playground.

5
  • Yes, it's expected. You told the compiler that the return type was any. Use number | { suit: string; card: number; } and it will only allow to return that. Same for your x argument. And write unit tests. Commented Mar 21, 2019 at 12:07
  • @JBNizet Yes you're right but it fix the issue with this example since the type are completely incompatible. Imagine a different use case with a union types on two types that share some properties we got the issue again. Commented Mar 21, 2019 at 12:14
  • The compiler can't possibly check each and every error you can make in your code. Write tests. Make code reviews. Commented Mar 21, 2019 at 12:16
  • Yes sure, but that's the purpose of a compiler too. Commented Mar 21, 2019 at 12:20
  • I've updated the examples with incompatible types. Commented Mar 21, 2019 at 12:25

1 Answer 1

2

The TypeScript compiler can't analyze the logic of the code to ensure it meets the contract. I'm surprised to see that it can't pick up the fact you're returning a string from a function that should return either a number or an object, but being able to do that may well be on the "To Do" list (there's a lot to do) and just hasn't gotten there yet. I would tend to think it would be fairly low priority, since it can't ensure correctness anyway, since it can't analyze the logical flow.

If you define a type for your cards, you can specify number | Card instead of any for the implementation:

const suits = ["hearts", "spades", "clubs", "diamonds"];
type Card = { suit: string; card: number; };

function pickCard(x: Card[]): number;
function pickCard(x: number): Card;
function pickCard(x): number | Card {
  if (typeof x == "object") {
    const cards = x as Card[];
    let pickedCard = Math.floor(Math.random() * cards.length);
    return pickedCard.toString();  // <========================== Compilation error
  } else if (typeof x == "number") {
    const n = x as number;
    let pickedSuit = Math.floor(n / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

const card = pickCard([{ suit: 'hearts', card: 5 }])

card.toFixed()

On the playground.

That can't help you with cases where you don't branch properly, but it can help you with this situation where you're returning a string from a function that should return a number or a card.

(Perhaps you could make it work without defining the type, but I couldn't immediately do so and in any case, all that retyping seems problematic.)

If the implementation is non-trivial, you might even combine that with my earlier suggestion of splitting the implementations off to their own typesafe functions:

const suits = ["hearts", "spades", "clubs", "diamonds"];
type Card = { suit: string; card: number; };

function pickCard(x: Card[]): number;
function pickCard(x: number): Card;
function pickCard(x): number | Card {
  if (typeof x == "object") {
    return pickCard_Cards(x as Card[]);
  } else if (typeof x == "number") {
    return pickCard_number(x as number);
  }
}
// These wouldn't be exported
function pickCard_Cards(cards: Card[]): number {
    let pickedCard = Math.floor(Math.random() * cards.length);
    return pickedCard.toString();  // <========================== Compilation error
}
function pickCard_number(n: number): Card {
    let pickedSuit = Math.floor(n / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
}

const card = pickCard([{ suit: 'hearts', card: 5 }])

card.toFixed()

On the playground

...so that the logic of each branch gets checked individually (you don't accidentally return a Card[] from the branch that should be returning a number and vice-versa).

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the answer! The thing is that if the return type of the private function is incorrect we got the same issue. We're moving the issue in another place.
@SamuelVaillant - Yes, but at least it helps you catch it if the implementation isnon-trivil. But worse than that, there's a better answer (but it combines well with my original approach). Updated. :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.