Apply the expert patterns that secure a real codebase: simulated nominality, validation without widening, and ambient declarations.
Open this lesson in KodokonTypeScript's typing is structural: two types with the same shape are interchangeable, so a user-id string is indistinguishable from any other string. Branding simulates nominal typing by intersecting the base type with a phantom property whose key is a unique symbol: no real value ever has it, but the compiler now tells each brand apart.
declare const brand: unique symbol;
type Brand<T, Name extends string> =
T & { readonly [brand]: Name };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function asUserId(raw: string): UserId {
return raw as UserId;
}
declare function loadUser(id: UserId): void;
loadUser(asUserId("u_42")); // OK
// loadUser("u_42"); // error: bare string rejectedThe satisfies operator (TypeScript 4.9) fills a precise gap. A : T annotation widens the variable's type to T and loses the inferred literals; an as T disables part of the checks. satisfies T verifies that the expression conforms to T without touching the inferred type, which stays as precise as possible. It is the ideal tool for configuration objects: full validation, precision intact.
type RouteDef = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
} satisfies Record<string, RouteDef>;
routes.admin.auth;
// literal type true: the inferred precision is
// preserved, and an unknown key would be rejectedA .d.ts file contains ambient declarations exclusively: it describes values that will exist at runtime, without emitting a single line of JavaScript. To extend the global scope from a module, the declare global block is mandatory - and the specification only allows it in a file that actually is a module, hence the export {} idiom at the top of the file.
export {};
declare global {
interface Window {
analytics: { track(event: string): void };
}
}For a JavaScript dependency without published types, declare module creates an ambient module: the compiler will use your declaration for every import of the package. Watch out for the scope of skipLibCheck: when enabled, it ignores errors from all .d.ts files, including yours; so verify your declarations through type tests placed in ordinary .ts files.
declare module "legacy-lib" {
export interface InitOptions {
debug?: boolean;
}
export function init(options?: InitOptions): void;
}satisfies T and a : T annotation?Brand<string, ...> contain?export {} at the top of a .d.ts file that uses declare global?