johnieskills

Fixes TypeScript errors and strengthens your types.

Resolve TypeScript errors, eliminate any, and design complex types (generics, conditional, mapped, template literal, branded/opaque). Use for type-inference problems, infer / extends questions, utility types (Partial, Record, ReturnType, Awaited, NoInfer), satisfies, function overloads, declaration merging, and strict-mode refactors.

$ npx skills add johnie/skills --skill typescript-engineer
skills/typescript-engineer/references/as-const-typeof.mdGitHub

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

  1. Immutability: Properties become readonly recursively
  2. Literal inference: Values are inferred as their literal types, not widened types
  3. 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