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!