Question
I am creating a TypeScript definition file for a third-party JavaScript library. One method accepts an options object, and its brace_style property may only use one of these strings: "collapse", "expand", "end-expand", or "none".
interface IOptions {
indent_size?: number;
indent_char?: string;
brace_style?: // "collapse" | "expand" | "end-expand" | "none"
}
Can a TypeScript interface enforce that, when brace_style is provided in an IOptions object, it can contain only a string from this allowed list?
Short Answer
You will learn how to use a string literal union type to restrict a TypeScript property to a fixed set of valid string values. This provides autocomplete, catches spelling mistakes at compile time, and documents the valid options directly in the type definition.
Concept
A normal string type accepts every possible text value:
brace_style?: string;
That is too broad when an API accepts only a known set of values. TypeScript can represent each exact string as a string literal type, then combine those types with | (the union operator):
type BraceStyle = "collapse" | "expand" | "end-expand" | "none";
A value of type BraceStyle must be exactly one of those four strings. Put this union on the interface property:
interface IOptions {
indent_size?: number;
indent_char?: string;
brace_style?: BraceStyle;
}
The ? means the property is optional. If it is omitted, the object is valid. If it is present, its value must match one of the allowed literal strings.
This is especially useful in declaration files and public APIs: consumers get editor suggestions and invalid configuration values are found before the program runs. TypeScript removes these types when compiling, so the underlying JavaScript library must still handle invalid values at runtime if untyped JavaScript callers are possible.
Mental Model
Think of string as permission to write anything on a form. A string literal union is a form with a dropdown menu containing only these choices:
"collapse""expand""end-expand""none"
The optional ? means the dropdown may be left blank. But if someone selects or types a value, it must be one of the listed choices.
Syntax and Examples
Use | to join exact string values:
type BraceStyle = "collapse" | "expand" | "end-expand" | "none";
interface IOptions {
indent_size?: number;
indent_char?: string;
brace_style?: BraceStyle;
}
Valid option objects:
const compact: IOptions = {
indent_size: 2,
indent_char: " ",
brace_style: "collapse"
};
const expanded: IOptions = {
brace_style: "end-expand"
};
const defaults: IOptions = {};
An invalid value causes a TypeScript error:
const invalid: IOptions = {
brace_style: "compact"
// Type error: "compact" is not assignable to type BraceStyle.
};
You may also write the union directly in the interface:
interface IOptions {
brace_style?: "collapse" | "expand" | "end-expand" | "none";
}
A named type such as BraceStyle is usually clearer when the same set of values is used in multiple places.
Step by Step Execution
Consider this code:
type BraceStyle = "collapse" | "expand" | "end-expand" | "none";
interface IOptions {
brace_style?: BraceStyle;
}
function formatCode(options: IOptions) {
console.log(options.brace_style ?? "collapse");
}
formatCode({ brace_style: "expand" });
formatCode({});
formatCode({ brace_style: "expanded" });
BraceStyledefines four exact allowed values.IOptions.brace_styleis optional because it has?.- The first call passes
"expand", which is in the union, so it type-checks. - The second call omits
brace_style, which is valid because the property is optional. - Inside the function,
?? "collapse"supplies a default when the property isundefined. - The final call fails type-checking because
"expanded"is not exactly"expand".
The error is reported during TypeScript checking; no runtime validation is automatically generated.
Real World Use Cases
String literal unions are useful whenever an API has a small, fixed vocabulary.
- UI variants:
"primary" | "secondary" | "danger" - HTTP methods:
"GET" | "POST" | "PUT" | "DELETE" - Sort order:
"asc" | "desc" - Theme selection:
"light" | "dark" | "system" - Request state:
"idle" | "loading" | "success" | "error" - Library configuration: formatter style, log level, animation mode, or cache strategy
For example, a button component can prevent an invalid visual variant:
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps {
label: string;
variant?: ButtonVariant;
}
Real Codebase Usage
In real projects, developers commonly name reusable unions and combine them with defaults, validation, and exhaustive handling.
Reuse a named option type
export type BraceStyle = "collapse" | "expand" | "end-expand" | "none";
export interface FormatterOptions {
brace_style?: BraceStyle;
}
Apply a default value
function normalizeOptions(options: FormatterOptions) {
return {
brace_style: options.brace_style ?? "collapse"
};
}
After normalization, brace_style is always a BraceStyle, not undefined.
Validate data received at runtime
Types protect TypeScript code, but API responses, JSON files, environment variables, and JavaScript callers can still provide unknown values.
const braceStyles = ["collapse", "expand", "end-expand", "none"] as const;
type BraceStyle = (typeof braceStyles)[number];
function isBraceStyle(value: string): value is BraceStyle {
return (braceStyles as readonly string[]).includes(value);
}
The as const form is useful because it creates both a runtime list and a matching union type.
Common Mistakes
Using string instead of a union
interface IOptions {
brace_style?: string;
}
This allows typos such as "colapse". Use the literal union instead.
Forgetting quotation marks around literal values
type BraceStyle = collapse | expand;
This is invalid unless collapse and expand are type names. String literal values need quotes:
type BraceStyle = "collapse" | "expand";
Using || when an explicit fallback matters
const style = options.brace_style || "collapse";
This works for the current union because all allowed strings are truthy, but ?? is more precise for optional values because it falls back only for null or undefined:
const style = options.brace_style ?? "collapse";
Assuming TypeScript validates runtime input
const style = JSON.parse(input).brace_style as BraceStyle;
A type assertion does not check the value. It only tells TypeScript to trust you. Validate untrusted data before treating it as BraceStyle.
Losing the literal type in a variable
let style = "collapse";
const options: IOptions = { brace_style: style };
// Error: style is inferred as string, not BraceStyle.
Give the variable the intended type:
let style: BraceStyle = "collapse";
const options: IOptions = { brace_style: style };
Comparisons
| Approach | What it permits | Best use |
|---|---|---|
string | Any string | Free-form text such as a user name or message |
"collapse" | "expand" | Only listed strings | Fixed configuration values and command names |
enum | Named enum members | When a named runtime object is also useful |
as const array + derived union | Listed values and a runtime array | When you need both validation data and a TypeScript type |
A TypeScript enum can model the same idea:
enum BraceStyle {
Collapse = "collapse",
Expand = "expand",
EndExpand = "end-expand",
None = "none"
}
For a simple string-based third-party API, a literal union is often the most direct choice because callers can write the exact strings expected by the JavaScript library.
enum also produces JavaScript output, while a type alias such as type BraceStyle = ... exists only during type checking.
Cheat Sheet
// Define a fixed set of valid strings
type BraceStyle = "collapse" | "expand" | "end-expand" | "none";
// Optional property: omitted or one allowed value
interface IOptions {
brace_style?: BraceStyle;
}
// Required property: must be one allowed value
interface RequiredOptions {
brace_style: BraceStyle;
}
// Valid
const a: IOptions = { brace_style: "collapse" };
const b: IOptions = {};
// Invalid
const c: IOptions = { brace_style: "collapsed" };
// Default an optional property
const style: BraceStyle = a.brace_style ?? "collapse";
// Derive a union from a runtime list
const styles = ["collapse", "expand", "end-expand", "none"] as const;
type DerivedBraceStyle = (typeof styles)[number];
Rules:
- Put literal strings in quotes.
- Join allowed alternatives with
|. - Use
?only when the property may be omitted. - Type aliases are compile-time only; validate untrusted runtime data separately.
FAQ
How do I restrict a string property to specific values in TypeScript?
Use a string literal union:
type Status = "pending" | "active" | "disabled";
Then declare the property as Status.
Can an optional TypeScript property have restricted string values?
Yes. Combine ? with the union:
brace_style?: "collapse" | "expand";
The property may be absent, but if present it must match an allowed value.
Should I put the union directly in the interface or create a type alias?
Both work. Create a named type alias when the values are reused, represent an important domain concept, or make the interface easier to read.
Does a string literal union create JavaScript code?
No. Type aliases and interfaces are erased during TypeScript compilation. They do not validate values at runtime.
Why does a variable containing "collapse" sometimes fail to assign?
A let variable may be inferred as the broad type string. Annotate it as BraceStyle, or use const when the value never changes.
Can I generate a union type from an array?
Yes, using as const:
const styles = ["collapse", "expand"] as const;
type BraceStyle = (typeof styles)[number];
This is useful when the same list is needed at runtime.
Is a string union better than an enum?
For values that directly match an external JavaScript API, string unions are often simpler. Use an enum when its runtime object and named members provide a clear benefit.
Mini Project
Description
Create a small formatter-options helper for a code formatting library. It accepts only supported brace styles, fills in defaults for omitted options, and demonstrates why compile-time types and runtime checks serve different purposes.
Goal
Define safe formatter options and return a normalized configuration with a guaranteed valid brace_style.
Requirements
- Define a
BraceStyletype with the four supported values. - Define an interface with optional indentation and brace-style settings.
- Create a function that applies defaults to omitted settings.
- Return an object whose
brace_styleis always a validBraceStyle. - Add a runtime helper that checks whether a string is a supported brace style.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.