Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.1: Improved Inference for Literal Types

TypeScript has had string literal types for a while. With TypeScript 2.0, the type system was extended by several new literal types:

  • Boolean literal types
  • Numeric literal types
  • Enum literal types

TypeScript 2.1 improves the type inference for all of these types when a const variable or readonly property has a literal initializer.

Better Inference for const Variables

Let's start with local variables and the var keyword. When TypeScript sees the following variable declaration, it infers the type string for the baseUrl variable:

var baseUrl = "https://example.com/";
// Inferred type: string

The same goes for variables declared with the let keyword:

let baseUrl = "https://example.com/";
// Inferred type: string

Both variables are inferred to have type string because they can change at any time. They are initialized with a literal string value, but they can be modified later.

However, if a variable is declared using the const keyword and initialized with a string literal, the inferred type is no longer string, but the corresponding string literal type:

const baseUrl = "https://example.com/";
// Inferred type: "https://example.com/"

The inferred type should be as specific as possible since the value of a constant string variable can never change. It's impossible for the baseUrl variable to hold any other value than "https://example.com/". This information is now reflected in the type system.

Literal type inference works for other primitive types, too. If a constant is initialized with an immediate numeric or boolean value, a literal type is inferred as well:

const HTTPS_PORT = 443;
// Inferred type: 443

const rememberMe = true;
// Inferred type: true

Similarly, a literal type is inferred when the initializer is an enum value:

enum FlexDirection {

const direction = FlexDirection.Column;
// Inferred type: FlexDirection.Column

Note that direction is typed as FlexDirection.Column, which is an enum literal type. Had we used the let or var keyword to declare the direction variable, its inferred type would've been FlexDirection instead.

Better Inference for readonly Properties

Similar to local const variables, readonly properties with a literal initializer are inferred to be of a literal type as well:

class ApiClient {
  private readonly baseUrl = "https://api.example.com/";
  // Inferred type: "https://api.example.com/"

  get(endpoint: string) {
    // ...

Read-only class properties can only be initialized right away or from within a constructor. Attempting to change the value in other places results in a compile-time error. Therefore, it is reasonable to infer a literal type for a read-only class property because its value doesn't change (given that the TypeScript program is type-correct).

Of course, TypeScript can't know what happens at run-time: properties marked with readonly can be changed at any time by some piece of JavaScript code. The readonly modifier is meant to restrict access to a property from within TypeScript code, but it has no run-time manifestation at all. That is, it is compiled away and doesn't show up in the generated JavaScript code.

Usefulness of Inferred Literal Types

You might ask yourself why it is useful to infer literal types for const variables and readonly properties. Consider the following code example:

const HTTP_GET = "GET"; // Inferred type: "GET"
const HTTP_POST = "POST"; // Inferred type: "POST"

function get(url: string, method: "GET" | "POST") {
  // ...

get("https://example.com/", HTTP_GET);

If the HTTP_GET constant was inferred to have type string instead of "GET", you'd get a compile-time error because you wouldn't be able to pass HTTP_GET as the second argument to the get function:

Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

Of course, it's not allowed to pass any arbitrary string as a function argument if the corresponding parameter only allows two specific string values. When the literal types "GET" and "POST" are inferred for the two constants, though, it all works out.

Next up: widening and non-widening literal types and the difference between the two.

This post is part of the TypeScript Evolution series:

  1. TypeScript 2.0: Non-Nullable Types
  2. TypeScript 2.0: Control Flow Based Type Analysis
  3. TypeScript 2.0: Acquiring Type Declaration Files
  4. TypeScript 2.0: Read-Only Properties
  5. TypeScript 2.0: Tagged Union Types
  6. TypeScript 2.0: More Literal Types
  7. TypeScript 2.0: The never Type
  8. TypeScript 2.0: Built-In Type Declarations
  9. TypeScript 2.1: async/await for ES3/ES5
  10. TypeScript 2.1: External Helpers Library
  11. TypeScript 2.1: Object Rest and Spread
  12. TypeScript 2.1: keyof and Lookup Types
  13. TypeScript 2.1: Mapped Types
  14. TypeScript 2.1: Improved Inference for Literal Types
  15. TypeScript 2.1: Literal Type Widening
  16. TypeScript 2.1: Untyped Imports
  17. TypeScript 2.2: The object Type
  18. TypeScript 2.2: Dotted Properties and String Index Signatures
  19. TypeScript 2.2: Null-Checking for Expression Operands
  20. TypeScript 2.2: Mixin Classes
  21. TypeScript 2.3: Generic Parameter Defaults
  22. TypeScript 2.3: The --strict Compiler Option
  23. TypeScript 2.3: Type-Checking JavaScript Files with --checkJs
  24. TypeScript 2.3: Downlevel Iteration for ES3/ES5
  25. TypeScript 2.4: String Enums
  26. TypeScript 2.4: Weak Type Detection
  27. TypeScript 2.4: Spelling Correction
  28. TypeScript 2.4: Dynamic import() Expressions
  29. TypeScript 2.5: Optional catch Binding
  30. TypeScript 2.6: JSX Fragment Syntax
  31. TypeScript 2.7: Numeric Separators
  32. TypeScript 2.7: Strict Property Initialization
  33. TypeScript 2.8: Per-File JSX Factories
  34. TypeScript 2.8: Conditional Types
  35. TypeScript 2.8: Mapped Type Modifiers