Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript type inference and literal types

Tags:

typescript

Why does Typescript infer the type string in the first example while it's able to infer the precise type union of type literals 'good' | 'bad' in the second example ?

const alwaysSomething = () => 'something' // inferred returned type: string
const moreComplicated = (mark: number) => mark >= 10 ? 'good' : 'bad' // inferred returned type:  'good' | 'bad'

On the playground. If you hover alwaysSomething, it shows its type is () => string, but if you hover moreComplicated, it shows its type is (mark: number) => "good" | "bad".

like image 409
Jean-Baptiste Rudant Avatar asked Sep 23 '19 11:09

Jean-Baptiste Rudant


1 Answers

The canonical answer to this question, if there is one, is likely to be found in the pull request implementing the literal widening algorithm. First we can see that the behavior you're seeing is what is intended:

In a function with no return type annotation, if the inferred return type is a literal type (but not a literal union type) and the function does not have a contextual type with a return type that includes literal types, the return type is widened to its widened literal type.

The inferred return type of () => 'something' is the literal "something", which is then widened to string. On the other hand, the inferred return type of (mark: number) => mark >= 10 ? 'good' : 'bad' is "good" | "bad", which is a literal union type, and is therefore not widened.

Why is a single-valued literal widened? There's this comment by the author:

[Y]ou practically never want the literal type. After all, why write a function that promises to always return the same value? Also, if we infer a literal type this common pattern is broken:

class Base {
  getFoo() {
      return 0;  // Default result is 0
  }
}

class Derived extends Base {
  getFoo() {
      // Compute and return a number
  }
}

If we inferred the type 0 for the return type in Base.getFoo, it would be an error to override it with an implementation that actually computes a number. You can of course add a type annotation if you really want to return a literal type, i.e. getFoo(): 0 { return 0; }.

And why is a literal union not widened? Here's a later comment by the author:

In the case of return cond ? 0 : 1 the inferred return type would be 0 | 1. I think in this case it is not at all clear we should widen to the base primitive type. After all, with a return type of 0 | 1 there is actually meaningful information being conveyed out of the function.

So the issue is one of practicality: people rarely intend to return single literal types, but they often do intend to return unions of literal types. Since type inference can always be overridden by explicit type annotations, you can deal with situations in which this heuristic gives you the wrong thing:

const alwaysSomething = (): 'something' => 'something' // inferred returned type: 
const moreComplicated = (mark: number): string => mark >= 10 ? 'good' : 'bad'

Okay, hope that helps. Good luck!

like image 114
jcalz Avatar answered Oct 22 '22 07:10

jcalz