Deriving Types from Runtime with as const and typeof
Overview
The combination of as const and typeof is one of the most powerful patterns in TypeScript for deriving types from runtime values. This pattern allows you to create a single source of truth that works at both runtime and compile time.
The as const Assertion
as const is a const assertion that makes an object deeply readonly and infers literal types instead of widened types.
// Without as const - types are widened
const config = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
};
// Type: { GROUP: string; ANNOUNCEMENT: string }
// With as const - literal types are preserved
const config = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
} as const;
// Type: { readonly GROUP: "group"; readonly ANNOUNCEMENT: "announcement" }
Key Benefits of as const
- Immutability: Properties become readonly recursively
- Literal inference: Values are inferred as their literal types, not widened types
- Array tuple inference: Arrays become readonly tuples with literal types
const routes = ["home", "about", "contact"] as const;
// Type: readonly ["home", "about", "contact"]
// Without as const: string[]
Pulling Runtime to Type World with typeof
Use typeof to extract the type from a runtime value:
const programModeEnumMap = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
ONE_ON_ONE: "1on1",
SELF_DIRECTED: "selfDirected",
} as const;
// Extract the type of the object
type ProgramMap = typeof programModeEnumMap;
// Extract keys as a union type
type BackendProgram = keyof typeof programModeEnumMap;
// Type: "GROUP" | "ANNOUNCEMENT" | "ONE_ON_ONE" | "SELF_DIRECTED"
// Extract values as a union type using indexed access
type FrontendProgram =
(typeof programModeEnumMap)[keyof typeof programModeEnumMap];
// Type: "group" | "announcement" | "1on1" | "selfDirected"
Pattern: Obj[keyof Obj] for Object Values
This pattern is like Object.values() for the type world:
const statusCodes = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
NOT_FOUND: 404,
} as const;
type StatusCode = (typeof statusCodes)[keyof typeof statusCodes];
// Type: 200 | 201 | 400 | 404
Pattern: Subset Selection with Union Index
You can select a subset of values by using a union of specific keys:
const programModeEnumMap = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
ONE_ON_ONE: "1on1",
SELF_DIRECTED: "selfDirected",
} as const;
type ProgramMap = typeof programModeEnumMap;
// Select only individual program types
type IndividualProgram = ProgramMap["ONE_ON_ONE" | "SELF_DIRECTED"];
// Type: "1on1" | "selfDirected"
When to Use This Pattern
- Configuration objects: Define config once, use it at runtime and compile time
- Enum alternatives: Create type-safe enums with string/number values
- Route definitions: Define routes with their metadata
- API mappings: Map between different representations (e.g., backend vs frontend)
- Event types: Define event names and their payloads
On TypeScript 5.8+ with --erasableSyntaxOnly, or under Node's native TypeScript execution and tsx, this stops being a preference: enum emits runtime code and is rejected outright, so the as const object is the only option. See typescript-versions.md.
The satisfies Operator (TS 4.9+)
satisfies checks that a value conforms to a type without widening the inferred type. It sits between annotation (widening) and no annotation (no validation).
Basic usage
type Color = "red" | "green" | "blue";
type Palette = Record<Color, [number, number, number] | string>;
// With annotation — loses literal info
const palette1: Palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
palette1.green.toUpperCase(); // Error: string | [number, number, number] has no toUpperCase
// With satisfies — validates shape, keeps narrowed types
const palette2 = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Palette;
palette2.green.toUpperCase(); // OK — inferred as string ✓
as const satisfies combo
Combine both to get literal types and shape validation:
const routes = {
home: "/",
user: "/users/:id",
post: "/posts/:postId",
} as const satisfies Record<string, `/${string}`>;
// Keys and values are literal types
type Route = (typeof routes)[keyof typeof routes];
// Type: "/" | "/users/:id" | "/posts/:postId"
// Shape is validated — this would error:
// const bad = { home: 123 } as const satisfies Record<string, `/${string}`>;
When to use each approach
| Approach | Validates shape | Preserves literals | Readonly |
|---|---|---|---|
const x: Type = … |
Yes | No | No |
const x = … as const |
No | Yes | Yes |
const x = … satisfies Type |
Yes | Yes | No |
const x = … as const satisfies Type |
Yes | Yes | Yes |
Common Pitfalls
Forgetting as const
Without as const, you lose literal type inference:
// BAD - values are widened to string
const colors = {
RED: "#ff0000",
GREEN: "#00ff00",
};
type Color = (typeof colors)[keyof typeof colors]; // string
// GOOD - literal types preserved
const colors = {
RED: "#ff0000",
GREEN: "#00ff00",
} as const;
type Color = (typeof colors)[keyof typeof colors]; // "#ff0000" | "#00ff00"
Attempting to Mutate
as const makes objects readonly - attempting to mutate will cause a compile error:
const config = {
timeout: 5000,
} as const;
config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property
Complete Example
// Single source of truth for HTTP methods
const HTTP_METHODS = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
} as const;
type HttpMethod = (typeof HTTP_METHODS)[keyof typeof HTTP_METHODS];
// Type: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
type SafeMethod = (typeof HTTP_METHODS)["GET"];
// Type: "GET"
type MutatingMethod = (typeof HTTP_METHODS)[
| "POST"
| "PUT"
| "DELETE"
| "PATCH"];
// Type: "POST" | "PUT" | "DELETE" | "PATCH"
function makeRequest(method: HttpMethod, url: string): void {
// method is type-safe
}
makeRequest(HTTP_METHODS.GET, "/api/users"); // OK
makeRequest("INVALID", "/api/users"); // Error