Template Literal Types
Template literal types construct string types from other types using backticks and ${...} interpolations. Unlike JavaScript template literals, they do not construct string values at runtime.
Basic Usage ≥0.339
1type Field = 'name' | 'email';2type EventName = `${Field}Changed`;3
4'nameChanged' as EventName; // OK5'emailChanged' as EventName; // OKEventName is the union 'nameChanged' | 'emailChanged'.
When to use this
Use template literal types for event names, string-based protocols, or strings with a required prefix or suffix. For unrelated constants, a plain union of literal types is simpler. Flow checks these annotations statically. They do not validate untrusted strings at runtime.
Literal Interpolation and Unions
Each interpolated literal contributes its string representation. Flow formats numbers by their value, not their source spelling, and omits the n suffix from bigints. Booleans contribute 'true' or 'false'; null becomes 'null', and void becomes 'undefined'.
1type Label = `${'item'}-${0xff}`;2type State = `enabled:${true | false}`;3type Id = `id:${42n}`;4type Missing = `${null | void}`;5
6'item-255' as Label; // OK7'enabled:false' as State; // OK8'id:42' as Id; // OK9'undefined' as Missing; // OK10'item-0xff' as Label; // ERROR: the number's value is 255incompatible-typeCannot cast 'item-0xff' to Label because "item-0xff" [1] is incompatible with Label [2].When you put unions in multiple placeholders, Flow produces every combination of their members. This is called a cross-product:
1type Name = `${'a' | 'b'}-${'x' | 'y'}`;2
3'a-x' as Name; // OK4'b-y' as Name; // OK5'a-z' as Name; // ERRORincompatible-typeCannot cast 'a-z' to Name because "a-z" [1] is incompatible with Name [2].Name is exactly 'a-x' | 'a-y' | 'b-x' | 'b-y'.
Open-ended String Patterns
The broad types string, number, and bigint describe sets of strings when used as placeholders. They do not make the template accept non-string values. A fixed prefix or suffix makes the template narrower than string, while ${string} alone accepts any string literal.
1type DataKey = `data-${string}`;2type NumericId = `item-${number}`;3type IntegerId = `item-${bigint}`;4
5'data-name' as DataKey; // OK6'item-1.5' as NumericId; // OK7'item-42' as IntegerId; // OK8'other-name' as DataKey; // ERROR: wrong prefixincompatible-typeCannot cast 'other-name' to DataKey because "other-name" [1] is incompatible with DataKey [2].9'item-many' as NumericId; // ERROR: not numeric textincompatible-typeCannot cast 'item-many' to NumericId because "item-many" [1] is incompatible with NumericId [2].10'item-1.5' as IntegerId; // ERROR: not integer textincompatible-typeCannot cast 'item-1.5' to IntegerId because "item-1.5" [1] is incompatible with IntegerId [2].11'item-42n' as IntegerId; // ERROR: the placeholder does not include nincompatible-typeCannot cast 'item-42n' to IntegerId because "item-42n" [1] is incompatible with IntegerId [2].12
13declare const key: DataKey;14key as string; // OKNumeric caveats
${number} is not a strict decimal, CSS, or range validator. It accepts exponent and radix forms, surrounding whitespace, and even a whitespace-only numeric portion. It rejects NaN, Infinity, and numeric separators.
${bigint} follows different lexical rules. It rejects fractions, exponents, whitespace, a leading +, decimal leading zeros, and separators.
1type NumericText = `${number}`;2
3'1e3' as NumericText; // OK4'0xff' as NumericText; // OK5' ' as NumericText; // OK6'NaN' as NumericText; // ERRORincompatible-typeCannot cast 'NaN' to NumericText because "NaN" [1] is incompatible with NumericText [2].7'Infinity' as NumericText; // ERRORincompatible-typeCannot cast 'Infinity' to NumericText because "Infinity" [1] is incompatible with NumericText [2].8'1_000' as NumericText; // ERRORincompatible-typeCannot cast '1_000' to NumericText because "1_000" [1] is incompatible with NumericText [2].Generic Inference
Flow can infer a type parameter from the part of a string matched by a placeholder:
1declare function stripPrefix<T extends string>(key: `data-${T}`): T;2
3stripPrefix('data-name') as 'name'; // OK4stripPrefix('data-name') as 'email'; // ERRORincompatible-typeCannot cast stripPrefix(...) to "email" because "name" [1] is incompatible with "email" [2].The declaration says that the function returns the suffix. You still need to implement that behavior at runtime.
Conditional Inference
In a conditional type, infer extracts the part of a string that matches a placeholder. The false branch determines the result for strings that do not match:
1type DataName<S extends string> = S extends `data-${infer Name}` ? Name : empty;2
3'name' as DataName<'data-name'>; // OK4'email' as DataName<'data-name'>; // ERRORincompatible-typeCannot cast 'email' to DataName<"data-name"> because "email" [1] is incompatible with "name" [2].5'name' as DataName<'name'>; // ERROR: the result is emptyincompatible-typeCannot cast 'name' to DataName<"name"> because "name" [1] is incompatible with empty [2].String Casing
You can combine templates with the existing Uppercase, Lowercase, Capitalize, and Uncapitalize utilities:
1type EventName = `on${Capitalize<'click' | 'change'>}`;2
3'onClick' as EventName; // OK4'onChange' as EventName; // OK5'onclick' as EventName; // ERRORincompatible-typeCannot cast 'onclick' to EventName because "onclick" [1] is incompatible with EventName [2].Templates as Mapped Keys
Use a template literal type in a mapped type's as clause to rename properties, such as turning mark into onmark. The key remapping example keeps each original property's value type. See filtering keys when the source also has symbol keys.
Limits
Placeholders must be compatible with string | number | bigint | boolean | null | void. Flow does not support object-like types or unknown as placeholders:
1type ObjectText = `${{name: string}}`; // ERRORinvalid-template-literal-typeTemplate literal type placeholder must be a subtype of string | number | bigint | boolean | null | undefined.2type UnknownText = `${unknown}`; // ERRORinvalid-template-literal-typeTemplate literal type placeholder must be a subtype of string | number | bigint | boolean | null | undefined.Flow will not expand a union cross-product with more than 10,000 combinations. Keep combinations small, or generate an explicit union ahead of time to avoid expanding a large set during type checking.
Adoption
To use template literal types, set experimental.tslib_syntax=true in .flowconfig and use Flow and flow-parser 0.339.0+ and Prettier 3.10.0+ with its built-in flow parser.
See Also
- Literal Types — specific primitive values as types
- Generics — parameterizing string patterns
- Conditional Types — extracting types with
infer - String Utilities — transforming casing
- Mapped Types — constructing object types from keys