Marius Schulz
Marius Schulz
Front End Engineer

Improved Inference for Literal Types in TypeScript

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 {
  Row,
  Column,
}

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/"

  request(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 request(url: string, method: "GET" | "POST") {
  // ...
}

request("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 request 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 article and 44 others are part of the TypeScript Evolution series. Have a look!