satisfies + as const cheatsheet.
The widening problem
const config = {
host: "localhost",
port: 5432,
};
// type: { host: string; port: number }
// — lost the literals
To keep "localhost" and 5432:
const config = {
host: "localhost",
port: 5432,
} as const;
// type: { readonly host: "localhost"; readonly port: 5432 }
satisfies (TS 4.9+)
type Config = Record<string, string | number>;
const config = {
host: "localhost",
port: 5432,
} satisfies Config;
// Validates against Config, but type stays literal:
// { host: "localhost"; port: 5432 }
vs annotation:
const config: Config = { host: "localhost", port: 5432 };
// type: Record<string, string | number> — too wide
Why prefer satisfies over annotation
type Routes = Record<string, { method: "GET" | "POST" }>;
const routes = {
users: { method: "GET" },
posts: { method: "POST" },
} satisfies Routes;
routes.users.method; // "GET" (literal)
routes.users; // exists (not optional)
With annotation : Routes, routes.users would be { method: "GET" | "POST" } | undefined.
satisfies + as const
const config = {
host: "localhost",
port: 5432,
} as const satisfies Config;
// Combine readonly literals + validation
Deriving types
const config = {
host: "localhost",
port: 5432,
} as const;
type Host = typeof config.host; // "localhost"
type Keys = keyof typeof config; // "host" | "port"
Enum replacement
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = typeof Status[keyof typeof Status];
// "active" | "inactive"
function f(s: Status) { ... }
f(Status.Active);
f("active");
Lighter than enum, no runtime code generated beyond the object.
Arrays as const
const days = ["mon", "tue", "wed"] as const;
type Day = typeof days[number]; // "mon" | "tue" | "wed"
for (const d of days) {
// d: "mon" | "tue" | "wed"
}
satisfies for function defaults
type Theme = { primary: string; bg: string };
const defaultTheme = {
primary: "#000",
bg: "#fff",
} satisfies Theme;
// Use defaultTheme.primary as known "#000"
// Pass it where a Theme is required
satisfies for handlers
type Handler = (req: Request) => Promise<Response>;
const handlers = {
users: async (req) => new Response("..."),
posts: async (req) => new Response("..."),
} satisfies Record<string, Handler>;
handlers.users; // (req: Request) => Promise<Response>
Inference from objects
function defineRoutes<T extends Record<string, unknown>>(routes: T): T {
return routes;
}
const r = defineRoutes({
users: { method: "GET" as const },
posts: { method: "POST" as const },
});
r.users.method; // "GET"
Generic identity function preserves literal inference.
NoInfer for hint-only types
function call<T>(value: T, onError: (e: NoInfer<T>) => void) { ... }
call("hi", (x) => x.toUpperCase());
// T inferred from "hi", not from the callback
Const type parameters (5.0+)
function arr<const T extends readonly any[]>(xs: T): T {
return xs;
}
const a = arr(["a", "b"]); // readonly ["a", "b"]
Like as const but on the function param.
When NOT to satisfy
function takeConfig(c: Config) { ... }
const cfg = {} satisfies Config;
If you’re going to pass it to a function that takes the wider type, the literal benefits don’t matter. Just annotate.
Typing JSON-like literals
const cfg = {
routes: [
{ path: "/", method: "GET" },
{ path: "/x", method: "POST" },
],
} satisfies {
routes: Array<{ path: string; method: "GET" | "POST" | "PUT" }>;
};
Catches typos in method while preserving the structure.
Common mistakes
: Typeinstead ofsatisfies Typewhen you want literal types preserved.as Typeinstead ofsatisfies Type—asis a cast, no check.- Forgetting
as const— array becomesstring[]instead of tuple. as conston values passed to mutable APIs — caller may push to yourreadonlyarray.
Read this next
If you want my config + routes helpers, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .