TypeScript as const
Guide
The as const
assertion in TypeScript is like putting a "do not change" label on your variables, ensuring their types are as specific as possible. Instead of widening types to general categories like string
or number
, it preserves the exact literal value as the type. This guide will cover practical uses and code examples of using as const
to solve common challenges in TypeScript development.
Introduction to as const
Understanding the as const
assertion is important for ensuring literal type inference, stricter type safety, and keeping constant values in arrays and objects. For example:
const status = "completed" as const;
// status is of type "completed"
In this example, TypeScript infers the type as the literal "completed"
rather than the more general string
type. This distinction becomes crucial when you need to ensure variables contain specific values only.
This type specificity offers several benefits:
- It prevents accidental reassignment to other values
- It enables more precise function parameter typing
- It helps TypeScript catch errors that might otherwise go unnoticed
When working with utility types
and type assertion, as const
becomes an essential tool in your TypeScript toolbox.
Using as const
for Literal Type Inference
When TypeScript infers types, it normally widens primitive values to their base types. For instance, a string literal becomes a string
, and a numeric literal becomes a number
. The as const
assertion prevents this widening behavior:
// Without as const
const color = "red";
// Type is widened to string
// With as const
const color = "red" as const;
// Type is preserved as literal "red"
This precise typing is valuable when working with functions that expect specific string literals:
// Define function accepting only specific theme options
function setTheme(theme: "light" | "dark" | "system") {
// Implementation
}
// Without as const
const userTheme = "light";
setTheme(userTheme); // Error: Argument of type 'string' is not assignable to parameter of type '"light" | "dark" | "system"'
// With as const
const userTheme = "light" as const;
setTheme(userTheme); // Works correctly
By using as const
, you're telling TypeScript to treat the value as a specific literal type rather than a general one. This helps catch type errors early and enables better autocompletion in your IDE.
This technique works seamlessly with keyof
when you need to reference specific property names in complex objects. When building end-to-end typed applications, check out Convex's approach to TypeScript.
Applying as const
for Stricter Type Safety
Using as const
with arrays creates readonly tuple types with specific literal elements:
// Without as const
const colors = ["red", "green", "blue"];
// Type: string[]
// With as const
const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
This creates two significant type safety improvements:
- The array becomes readonly, preventing mutations like
push()
orpop()
- The exact elements are preserved as literal types
This approach is ideal for defining sets of allowed values:
const VALID_STATUSES = ["pending", "processing", "completed"] as const;
type Status = typeof VALID_STATUSES[number];
// Status type is: "pending" | "processing" | "completed"
// Now you can use this type in your functions
function updateStatus(newStatus: Status) {
// Implementation
}
// Error: Argument of type '"rejected"' is not assignable to parameter of type 'Status'
updateStatus("rejected");
When building complex applications, this pattern ensures data validation and consistency across your codebase. It also works well with type narrowing
techniques.
Converting Objects to Readonly Types with as const
You can use as const
to convert an object to a readonly type. For instance:
// Without as const
const config = { mode: "dark", fontSize: 16 };
// Type: { mode: string; fontSize: number; }
// With as const
const config = { mode: "dark", fontSize: 16 } as const;
// Type: { readonly mode: "dark"; readonly fontSize: 16; }
This immutability prevents accidental property changes:
const settings = {
theme: "dark",
notifications: {
email: true,
push: false
}
} as const;
// Error: Cannot assign to 'theme' because it is a read-only property
settings.theme = "light";
// Error: Cannot assign to 'email' because it is a read-only property
settings.notifications.email = false;
You can also use type inference with as const
objects:
const API_ENDPOINTS = {
users: "/api/users",
posts: "/api/posts",
comments: "/api/comments"
} as const;
// Extract the endpoint values as a union type
type Endpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];
// Type: "/api/users" | "/api/posts" | "/api/comments"
This pattern works well for creating type-safe configurations in backend APIs with Convex and frontend applications. You can also combine this with readonly
for additional type safety.
Preventing Accidental Type Widening with as const
Type widening occurs when TypeScript broadens a literal type to its base type. For example, when passing literals to functions or using them in conditional logic, TypeScript might widen their types:
// Function that takes a callback
function fetchData(onSuccess: (status: string) => void) {
// Implementation
}
// The specific string literal type is lost when passed as a parameter
fetchData((status) => {
// Here, status is typed as 'string', not as specific values
if (status === "success") {
// ...
}
});
You can use as const
to prevent this widening behavior:
const STATUSES = {
SUCCESS: "success",
ERROR: "error",
LOADING: "loading"
} as const;
function handleStatus(status: typeof STATUSES[keyof typeof STATUSES]) {
// status is typed as: "success" | "error" | "loading"
}
// No type widening occurs, the literal types are preserved
handleStatus(STATUSES.SUCCESS);
This technique is crucial when performing type narrowing in conditional blocks and switch statements. By preserving the literal types, you ensure TypeScript can accurately track the possible values throughout your code. For best practices with TypeScript in your projects, check out Convex's TypeScript guidelines.
Maintaining Constant Values in Arrays and Objects with as const
Using as const
can help maintain constant values in arrays and objects. Consider this example:
// Define roles as constants
const ROLES = ["admin", "user", "guest"] as const;
type Role = typeof ROLES[number];
// Role type is: "admin" | "user" | "guest"
// Define permission levels with associated values
const PERMISSIONS = {
READ: 1,
WRITE: 2,
DELETE: 4,
ADMIN: 8
} as const;
type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS];
// Permission type is: 1 | 2 | 4 | 8
You can then use these constants for bit-wise operations while maintaining type safety:
function hasPermission(userPermission: number, requiredPermission: Permission): boolean {
return (userPermission & requiredPermission) !== 0;
}
const userPermission = PERMISSIONS.READ | PERMISSIONS.WRITE; // 3
console.log(hasPermission(userPermission, PERMISSIONS.WRITE)); // true
console.log(hasPermission(userPermission, PERMISSIONS.DELETE)); // false
This pattern integrates well with Convex database schemas and validation where you need to enforce specific allowed values. It's also useful when working with enum
alternatives.
Improving Type Accuracy with the as const
Assertion
The as const
assertion helps improve type accuracy in TypeScript. For instance:
// Define configuration options
const config = {
theme: {
primary: "#0066cc",
secondary: "#ff9900",
background: "#ffffff"
},
api: {
baseUrl: "https://api.example.com",
timeout: 5000
}
} as const;
// Type for accessing config values
type ThemeColor = keyof typeof config.theme;
// ThemeColor type is: "primary" | "secondary" | "background"
// Function to get a theme color
function getColor(colorName: ThemeColor): string {
return config.theme[colorName];
}
// Works correctly
getColor("primary");
// Type error: Argument of type '"accent"' is not assignable to parameter of type 'ThemeColor'
getColor("accent");
This approach creates a self-documenting system where the TypeScript compiler enforces valid property access. It ensures that only existing properties can be referenced, reducing runtime errors.
When building complex applications with Convex database relationships, using as const
with configuration objects helps maintain consistency throughout your codebase. This works particularly well with Record<K, T>
types for mapping configurations.
Keeping TypeScript Literals Immutable with as const
To keep TypeScript literals immutable, use the as const
assertion. For example:
// Simple string literal
const API_KEY = "abc123" as const;
// Error: Cannot assign to 'API_KEY' because it is a constant
API_KEY = "xyz789";
// Object with nested properties
const APP_CONFIG = {
version: "1.0.0",
features: {
darkMode: true,
notifications: {
email: true,
push: true
}
}
} as const;
// Error: Cannot assign to 'version' because it is a read-only property
APP_CONFIG.version = "1.0.1";
// Error: Cannot assign to 'push' because it is a read-only property
APP_CONFIG.features.notifications.push = false;
This immutability extends to arrays as well:
const ALLOWED_ORIGINS = ["https://example.com", "https://api.example.com"] as const;
// Error: Property 'push' does not exist on type 'readonly ["https://example.com", "https://api.example.com"]'
ALLOWED_ORIGINS.push("https://dev.example.com");
// Error: Cannot assign to '0' because it is a read-only property
ALLOWED_ORIGINS[0] = "https://new-example.com";
When working with Convex server functions, this immutability ensures configuration values remain consistent throughout your application's lifecycle. You can also combine this with interface
definitions for more complex type structures.
Common Challenges and Solutions
When working with TypeScript, developers encounter several recurring challenges that as const
can help solve. Let's explore some practical solutions to these common issues.
Working with Discriminated Unions
Discriminated unions benefit greatly from as const
when defining type discriminators:
// Define event types with a "type" property as discriminator
const EVENT_TYPES = {
CLICK: "click",
SUBMIT: "submit",
LOAD: "load"
} as const;
type EventType = typeof EVENT_TYPES[keyof typeof EVENT_TYPES];
// Create discriminated union
type ClickEvent = {
type: typeof EVENT_TYPES.CLICK;
element: string;
x: number;
y: number;
};
type SubmitEvent = {
type: typeof EVENT_TYPES.SUBMIT;
formData: Record<string, string>;
};
type LoadEvent = {
type: typeof EVENT_TYPES.LOAD;
duration: number;
};
type AppEvent = ClickEvent | SubmitEvent | LoadEvent;
// Type-safe event handler
function handleEvent(event: AppEvent) {
switch (event.type) {
case EVENT_TYPES.CLICK:
// TypeScript knows event is ClickEvent here
console.log(`Clicked ${event.element} at (${event.x}, ${event.y})`);
break;
case EVENT_TYPES.SUBMIT:
// TypeScript knows event is SubmitEvent here
console.log(`Form submitted with data:`, event.formData);
break;
case EVENT_TYPES.LOAD:
// TypeScript knows event is LoadEvent here
console.log(`Page loaded in ${event.duration}ms`);
break;
}
}
This approach with as const
ensures type safety through the entire flow of your application when working with complex filters in Convex queries. It's particularly effective when combined with discriminated union
patterns.
Using as const
with Generic Functions
When working with generic functions, you might need to preserve literal types:
function createState<T>(initialValue: T) {
let value = initialValue;
return {
get: () => value,
set: (newValue: T) => { value = newValue; }
};
}
// Without as const
const counter = createState({ count: 0 });
// Type is { get: () => { count: number }; set: (newValue: { count: number }) => void }
// With as const
const counter = createState({ count: 0 } as const);
// Type is { get: () => { readonly count: 0 }; set: (newValue: { readonly count: 0 }) => void }
// This works with as const because the exact type is preserved
counter.set({ count: 0 });
// This fails with as const, protecting against invalid updates
counter.set({ count: "zero" }); // Error: Type 'string' is not assignable to type 'number'
This pattern is useful when building state management systems with Convex's useState-less approach and when working with generics
in your application.
Final Thoughts on TypeScript as const
The as const
assertion enhances type safety by preserving literal types and enforcing immutability. Use it whenever you need precise type checking or want to prevent accidental modifications to your variables. It works especially well for constants, configuration objects, and discriminated unions.
For more advanced TypeScript patterns, check out Convex's TypeScript best practices and consider using satisfies
for additional type validation with as const
objects.