Type Inference
How TypeScript infers types from assignments - const vs let narrowing, as const, and return type inference.
TypeScript infers types from the values you assign - you rarely need to annotate every variable. But inference has rules, and knowing when it widens a type vs narrows it prevents subtle bugs.
const infers a narrow literal type. let infers a wide primitive type because the value can change.
const lang = "typescript"; // type: "typescript" (string literal)
let lang2 = "typescript"; // type: string (widened)
const count = 42; // type: 42 (numeric literal)
let count2 = 42; // type: number (widened)Object and array literals inferred with const still get wide types for their properties - the object reference is const, but the properties are mutable. Use as const to lock everything down to literal types.
const config = { env: "production", port: 3000 };
// config.env: string (NOT "production")
// config.port: number (NOT 3000)
const config2 = { env: "production", port: 3000 } as const;
// config2.env: "production"
// config2.port: 3000
// All properties are readonly - config2.env = "staging" is a type errorTypeScript infers function return types automatically. Annotating the return type explicitly is optional but documents intent and catches mistakes when the function body changes.
// inferred return type: number
function add(a: number, b: number) {
return a + b;
}
// explicit return type - documents intent, errors if the body returns something else
function greet(name: string): string {
return `Hello, ${name}!`;
}
// explicit return type: Promise<number>
async function fetchCount(): Promise<number> {
return 42;
}In production
The widening of object literals is one of the most common TypeScript footguns in configuration code. const config = { env: 'production' } gives config.env the type string, not 'production'. When this config object is passed to a function expecting { env: 'production' | 'staging' }, TypeScript rejects it with a confusing error. Fix it at the source with as const or an explicit type annotation. This pattern appears constantly in feature flags, environment configs, and route definitions - getting it right once prevents a class of "works at runtime but fails type checking" surprises.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free