Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.1: Literal Type Widening

In my previous post about better type inference in TypeScript 2.1, I explained how TypeScript infers literal types for const variables and readonly properties with literal initializers. This post continues this discussion and draws a difference between widening and non-widening literal types.

Widening Literal Types

When you declare a local variable using the const keyword and initialize it with a literal value, TypeScript will infer a literal type for that variable:

const stringLiteral = "https"; // Type "https"
const numericLiteral = 42;     // Type 42
const booleanLiteral = true;   // Type true

Because of the const keyword, the value of each variable cannot be changed later, so a literal type makes perfect sense. It preserves information about the exact value that was assigned.

If you take the constants defined above and assign them to let variables, each of the literal types will be widened to the respective widened type:

let widenedStringLiteral = stringLiteral;   // Type string
let widenedNumericLiteral = numericLiteral; // Type number
let widenedBooleanLiteral = booleanLiteral; // Type boolean

In contrast to variables declared using the const keyword, variables declared using the let keyword can be changed later on. They are usually initialized with a certain value and mutated afterwards. If TypeScript were to infer a literal type for such let variables, trying to assign any other value than the specified literal would produce an error at compile-time.

For this reason, widened types are inferred for each of the above let variables. The same goes for enum literals:

enum FlexDirection {

const enumLiteral = FlexDirection.Row; // Type FlexDirection.Row
let widenedEnumLiteral = enumLiteral; // Type FlexDirection

To summarize, here are the rules for widening literal types:

So far we've been looking at widening literal types which are automatically widened when necessary. Let's now look at non-widening literal types which, as their name suggests, are not widened automatically.

Non-Widening Literal Types

You can create a variable of a non-widening literal type by explicitly annotating the variable to be of a literal type:

const stringLiteral: "https" = "https"; // Type "https" (non-widening)
const numericLiteral: 42 = 42; // Type 42 (non-widening)

Assigning the value of a variable that has a non-widening literal type to another variable will not widen the literal type:

let widenedStringLiteral = stringLiteral; // Type "https" (non-widening)
let widenedNumericLiteral = numericLiteral; // Type 42 (non-widening)

Notice how the types are still "https" and 42. Unlike before, they haven't been widened to string and number, respectively.

Usefulness of Non-Widening Literal Types

To understand why non-widening literals can be useful, let's look at widening literal types once again. In the following example, an array is created from two variables of a widening string literal type:

const http = "http"; // Type "http" (widening)
const https = "https"; // Type "https" (widening)

const protocols = [http, https]; // Type string[]

const first = protocols[0]; // Type string
const second = protocols[1]; // Type string

TypeScript infers the type string[] for the array. Therefore, array elements like first and second are typed as string. The notion of the literal types "http" and "https" got lost in the widening process.

If you were to explicitly type the two constants as "http" and "https", the protocols array would be inferred to be of type ("http" | "https")[] which represents an array that only contains the strings "http" or "https":

const http: "http" = "http"; // Type "http" (non-widening)
const https: "https" = "https"; // Type "https" (non-widening)

const protocols = [http, https]; // Type ("http" | "https")[]

const first = protocols[0]; // Type "http" | "https"
const second = protocols[1]; // Type "http" | "https"

Both first and second are typed as "http" | "https" now. This is because the array type doesn't encode the fact that the value "http" is at index 0 while "https" is at index 1. It just states that the array only contains values of the two literal types, no matter at which position. It also doesn't say anything about the length of the array.

If, for some reason, you wanted to retain the position information of the string literal types in the array, you could explicitly type the array as a two-element tuple:

const http = "http"; // Type "http" (widening)
const https = "https"; // Type "https" (widening)

const protocols: ["http", "https"] = [http, https]; // Type ["http", "https"]

const first = protocols[0]; // Type "http" (non-widening)
const second = protocols[1]; // Type "https" (non-widening)

Now, first and second are inferred to be of their respective non-widening string literal type.

Further Reading

If you'd like to read more about the rationale behind widening and non-widening types, check out these discussions and pull requests on GitHub:

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