TypeScript 10 ๐ท Type Inference and Type Annotations
TypeScript gives you two ways to put a type on something: inference โ the compiler figures it out from the value โ and annotation โ you write it explicitly. Both are essential. Inference is why TypeScript feels light compared to Java or C#; annotations are why it’s precise when it needs to be. Knowing when to rely on inference and when to annotate is the difference between writing types that help and writing types that get in the way.
Key point: TypeScript infers most types. You don’t annotate local variables, simple constants, or the results of well-typed expressions. You annotate boundaries โ function parameters, public return types, empty variables, and anything where inference would produce a type wider or narrower than you want. Annotating everything is noise; annotating nothing is unsafe. The skill is knowing which is which.
What inference is
Inference is TypeScript determining a type from the surrounding context โ the initial value of a variable, the return of a function, the argument of a callback, the shape of an object literal.
const name = 'Alice'; // string
const age = 30; // number
const active = true; // boolean
const names = ['Alice', 'Bob']; // string[]
const user = { id: 1, name: 'Alice' }; // { id: number; name: string }
You wrote no types. TypeScript worked them out.
Where inference comes from:
- Initial values โ
const x = 5โnumber - Function returns โ return type inferred from the body
- Object literals โ shape inferred from properties
- Array literals โ element type inferred from elements
- Callback parameters โ inferred from the expected function type
- Generics โ inferred from arguments
- Contextual typing โ inferred from where the value is used
That’s a lot of inference. It’s why writing TypeScript feels like writing JavaScript with occasional annotations.
Why inference is the default: TypeScript’s team designed the language so that most code needs no type annotations at all. You get type safety for free in exchange for annotating the boundaries where inference can’t reach โ parameters of exported functions, empty containers, callbacks whose types aren’t apparent. The language is meant to feel like JavaScript most of the time.
Inference for variables
Variables get their type from the initializer.
let a = 'hello'; // string
let b = 42; // number
let c = true; // boolean
const d = 'hello'; // 'hello' (literal)
const e = 42; // 42 (literal)
let vs const โ widening:
let widens the literal to its general type. const keeps the literal.
let x = 'hello'; // string
const y = 'hello'; // 'hello'
x = 'world'; // โ
still string
// y = 'world'; // โ y is 'hello'
Why the difference: let is designed to be reassigned, so a literal type would be too restrictive. const can’t be reassigned, so the exact literal is preserved.
Object properties and widening:
const user = {
name: 'Alice', // string
role: 'admin' // string โ NOT 'admin'
};
Even with const, object properties are mutable โ so they widen. If you want literal types on properties, use as const:
const user = {
name: 'Alice',
role: 'admin'
} as const;
// { readonly name: 'Alice'; readonly role: 'admin' }
Arrays widen too:
const names = ['Alice', 'Bob']; // string[]
const roles = ['admin', 'user']; // string[] โ not ('admin' | 'user')[]
Use as const for literal element types.
Empty arrays and objects:
const empty = []; // any[]
const emptyObj = {}; // {}
Both are effectively useless without annotation. Always annotate empty collections.
Union widening:
let id = Math.random() > 0.5 ? 'a' : 1; // string | number
TypeScript infers the union of the branches.
Why
letwidens andconstdoesn’t: The compiler assumes you’ll reassignletvariables, so the literal type would be wrong most of the time.constis immutable by definition, so the literal is safe to preserve. This is whyas constexists โ to opt into literal types when the object structure isconstbut the individual properties aren’t.
Inference for function returns
A function’s return type is inferred from its body.
function add(a: number, b: number) {
return a + b; // inferred: number
}
function greet(name: string) {
return `Hello, ${name}`; // inferred: string
}
function makeUser(name: string) {
return { id: crypto.randomUUID(), name }; // inferred object shape
}
The return type flows from return statements.
Multiple return paths produce a union:
function find(id: number) {
if (id === 1) return { id: 1, name: 'Alice' };
return null;
}
// inferred: { id: number; name: string } | null
Each return contributes to the union.
Recursion breaks inference:
function factorial(n: number) {
return n <= 1 ? 1 : n * factorial(n - 1); // โ implicit any
}
The function calls itself before its return type is inferred, so TypeScript can’t determine it. Annotate:
function factorial(n: number): number {
return n <= 1 ? 1 : n * factorial(n - 1); // โ
}
Why annotate returns on exported functions: A public function’s return type is part of its contract. If a library’s getUser() is inferred as { id: number; name: string }, and later the implementation adds email, the inferred type changes โ a breaking change for consumers. Annotating the return pins the contract. Inference stays local; annotations define the boundary.
Async functions infer Promise<T>:
async function fetchUser(id: number) {
const res = await fetch(`/users/${id}`);
return res.json(); // inferred: Promise<any> โ because json() is any
}
Annotate the result to keep it precise:
interface User { id: number; name: string; }
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/users/${id}`);
return res.json() as Promise<User>;
}
Why recursion breaks inference: The compiler processes a function by looking at its return statements. A recursive call is a reference to the function itself โ but the function’s return type isn’t known yet. An annotation breaks the cycle. This is why recursive functions need explicit returns.
Inference for function parameters โ contextual typing
Function parameters are not inferred from usage. But they are inferred from context โ the expected type where the function is passed.
const nums = [1, 2, 3];
nums.map(n => n * 2); // n is number โ inferred from Array<number>.map
nums.filter(n => n % 2 === 0); // n is number โ inferred
const handler: (e: MouseEvent) => void = (e) => {
// e is MouseEvent โ inferred from the annotation on handler
};
This is contextual typing. The parameter type comes from the type of the function being assigned or passed.
When contextual typing doesn’t apply:
function double(n) { return n * 2; } // โ n is implicit any
A standalone function has no context. Annotate:
function double(n: number): number { return n * 2; }
Contextual typing with event handlers:
button.addEventListener('click', (e) => {
// e is MouseEvent โ inferred from addEventListener's signature
});
input.addEventListener('input', (e) => {
// e is Event โ not the specific type, because 'input' events are generic Event
});
For DOM events, addEventListener has overloads that narrow the event type based on the event name. TypeScript picks the right one.
Callbacks with generics:
function map<T, U>(xs: T[], fn: (x: T) => U): U[] {
return xs.map(fn);
}
map([1, 2, 3], x => `${x}`); // x is number, return U inferred as string
Both T and U are inferred from arguments โ T from the array, U from the callback’s return.
Why contextual typing is the big win: In JavaScript and TypeScript, callbacks are everywhere โ
.map,.filter, event handlers, promise chains. Without contextual typing, every callback parameter would need an annotation. With it,.map(n => n * 2)is fully typed with no boilerplate. Contextual typing is what makes TS feel ergonomic.
Inference for object literals
Object literals infer their shape from the properties.
const user = {
id: 1,
name: 'Alice',
active: true
};
// { id: number; name: string; active: boolean }
The shape is the set of properties, each inferred individually.
Properties widen:
const config = {
mode: 'dark', // string
retries: 3, // number
debug: false // boolean
};
mode is string, not 'dark'. To keep the literal, use as const.
Nested objects infer recursively:
const order = {
id: 'o-1',
customer: {
id: 'c-1',
name: 'Alice'
},
items: [{ sku: 'A', qty: 2 }]
};
The whole shape is inferred, including customer and items.
Optional properties don’t infer as optional:
const user = { id: 1 };
// { id: number } โ no optional properties inferred
If you want an optional property, annotate with an interface or add ?:
interface User { id: number; name?: string; }
const user: User = { id: 1 }; // โ
name is optional
Excess properties in literals:
interface User { id: number; name: string; }
const u: User = { id: 1, name: 'Alice', extra: true }; // โ excess 'extra'
When an object literal is annotated with a type, excess checks apply. Without annotation, extras are simply part of the inferred type.
Why object literals widen property types: The same reason
letwidens โ the object is mutable, so properties can change.{ mode: 'dark' }is mutable, somodeisstring.as constopts out of both mutability and widening. For inference, the wide type is the safe default.
When to annotate
You annotate when inference isn’t enough or produces the wrong type.
1. Function parameters โ always:
function greet(name: string): string { ... } // โ
name must be annotated
Without annotation, parameters are implicit any under strict (or any silently without).
2. Public function returns โ usually:
export function fetchUser(id: number): Promise<User> { ... } // โ
contract pinned
Private/local functions can rely on inference.
3. Empty variables:
let result: string[] = []; // โ
let user: User; // โ
Inference gives any[] or any.
4. Class properties that can’t be inferred:
class User {
name: string; // โ
annotated โ no initializer
id: number; // โ
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
With strictPropertyInitialization, class properties without initializers must be annotated and initialized in the constructor.
5. When inference gives something too wide or too narrow:
let status = 'idle'; // string โ too wide
let status: 'idle' | 'loading' = 'idle'; // โ
precise
6. Function types as variables:
const handler: (e: Event) => void = (e) => { ... };
Without the annotation, e would be implicit any.
7. When you want documentation:
interface User { id: number; name: string; } // named type documents intent
const user: User = { id: 1, name: 'Alice' };
The annotation documents what the shape is supposed to be.
When NOT to annotate:
- Local variables with obvious initializers
- Return types of private helpers
- Object literals assigned directly to a typed variable
- Callback parameters in a typed context
- Array methods with obvious results
Why the “boundary” rule: Type annotation is documentation and a contract. You want it at the boundaries โ parameters, exports, empty containers โ because those are where mistakes get made and where other code depends on the type. Inside a function, local inference is safe and better; annotating every local is noise. The boundary rule captures when annotation pays off.
Explicit annotations override inference
If you annotate, TypeScript uses your annotation instead of inference.
const name: string = 'Alice'; // annotation, not inference
const n: number = 5;
Annotations also constrain the value:
const mode: 'light' | 'dark' = 'light'; // โ
const mode2: 'light' | 'dark' = 'medium'; // โ
Annotations on object literals check excess properties:
interface User { id: number; name: string; }
const user: User = {
id: 1,
name: 'Alice',
age: 30 // โ excess property 'age'
};
Without the annotation, age would be part of the inferred type.
Annotations give better errors:
function getUser(): User {
return { id: 1 }; // โ missing 'name'
}
The error mentions User and names the missing property. Without the return annotation, the error would be less clear.
Annotations narrow the type of assignment:
const x: number | string = 'hello'; // type is number | string
x.toUpperCase(); // โ not on number
Even though the value is a string, the type is the annotation. TypeScript tracks the assigned type, not the inferred one. Use narrowing to use it.
Why annotations override: The annotation is a contract โ a promise about the value’s type. TypeScript trusts it and checks the value against it. That’s why
const x: number | string = 'hello'givesxthe union type even though it’s assigned a string. The annotation is the type; the value must conform.
as const โ opting out of widening
as const is the escape hatch for widening. It makes the literal stay literal and the object readonly.
const config = {
mode: 'dark',
retries: 3
};
// { mode: string; retries: number }
const strict = {
mode: 'dark',
retries: 3
} as const;
// { readonly mode: 'dark'; readonly retries: 3 }
Arrays:
const colors = ['red', 'green', 'blue'] as const;
// readonly ['red', 'green', 'blue']
Element types are the literals, and the array is readonly.
Extracting union from array:
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];
// 'red' | 'green' | 'blue'
typeof colors[number] is a common pattern to extract the element union.
When to use as const:
- Constant tables
- Configuration objects
- Event name lists
- Anywhere the literals matter
When not to:
- Mutable data
- Objects you’ll modify
- When widening is fine
Why
as constexists: Inference widens literals to their general types by default. For most code that’s correct โ you wantmode: stringbecause you’ll change it. For constants, the literals matter.as constsays “this object won’t change, so preserve its exact shape.”
A full example
A small program that shows inference and annotation working together.
// ============================================
// INFERENCE โ no annotations needed
// ============================================
const name = 'Alice'; // string
const age = 30; // number
const tags = ['dev', 'ts']; // string[]
function add(a: number, b: number) {
return a + b; // inferred: number
}
const user = {
id: 1,
name: 'Alice'
};
// inferred: { id: number; name: string }
// ============================================
// CONTEXTUAL TYPING โ parameters inferred from context
// ============================================
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // n: number
const even = nums.filter(n => n % 2 === 0); // n: number
// ============================================
// ANNOTATIONS โ at boundaries
// ============================================
interface User {
id: number;
name: string;
nickname?: string;
}
// Public function โ annotate parameters and return
function displayName(user: User): string {
return user.nickname ?? user.name;
}
// Empty container โ annotate
const results: string[] = [];
// Function-typed variable โ annotate
const log: (msg: string) => void = (msg) => console.log(msg);
// ============================================
// `as const` โ preserve literals
// ============================================
const STATUS = {
Idle: 'idle',
Loading: 'loading',
Ready: 'ready'
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
// 'idle' | 'loading' | 'ready'
// ============================================
// USING EVERYTHING
// ============================================
const alice: User = { id: 1, name: 'Alice' };
results.push(displayName(alice));
log(`Got ${results.length} result(s)`);
console.log(doubled, even, STATUS.Idle);
What’s inferred: name, age, tags, user, add‘s return, nums.map callback parameter.
What’s annotated: add‘s parameters, displayName‘s parameters and return, results as string[], log as a function type, alice as User.
What’s as const: STATUS โ the object’s values become literal types, and Status is derived from it.
Why this split: The compiler infers what it can, you annotate the boundaries, and
as consthandles the constant case. Nothing is over-annotated; nothing critical is left to inference. That balance is the goal.
Complete Example Session
# ============================================
# PART 1: BASIC INFERENCE
# ============================================
cat > inference.ts << 'EOF'
const name = 'Alice';
const age = 30;
const active = true;
const names = ['Alice', 'Bob'];
const user = { id: 1, name: 'Alice' };
// let widens, const keeps literal
let a = 'hello'; // string
const b = 'hello'; // 'hello'
function add(x: number, y: number) {
return x + y; // inferred: number
}
console.log(name, age, active, names, user, a, b, add(1, 2));
EOF
npx tsc --noEmit inference.ts
# (no errors)
# ============================================
# PART 2: CONTEXTUAL TYPING
# ============================================
cat > contextual.ts << 'EOF'
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);
const even = nums.filter(n => n % 2 === 0);
const strings = nums.map(n => `${n}`);
const handler: (e: Event) => void = (e) => {
console.log(e.type);
};
console.log(doubled, even, strings);
EOF
npx tsc --noEmit contextual.ts
# (no errors)
# ============================================
# PART 3: WHEN TO ANNOTATE
# ============================================
cat > annotate.ts << 'EOF'
interface User {
id: number;
name: string;
nickname?: string;
}
// Annotate function parameters and returns
function display(user: User): string {
return user.nickname ?? user.name;
}
// Annotate empty containers
const results: string[] = [];
results.push(display({ id: 1, name: 'Alice' }));
// Annotate function-typed variables
const log: (msg: string) => void = (msg) => console.log(msg);
log('done');
console.log(results);
EOF
npx tsc --noEmit annotate.ts
# (no errors)
# ============================================
# PART 4: `as const`
# ============================================
cat > as-const.ts << 'EOF'
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'
const config = {
mode: 'dark',
retries: 3
} as const;
function setColor(c: Color): void {
console.log('Set to', c);
}
setColor('red');
// setColor('purple'); // โ
console.log(colors, config);
EOF
npx tsc --noEmit as-const.ts
# (no errors)
# ============================================
# PART 5: ANNOTATIONS OVERRIDE
# ============================================
cat > override.ts << 'EOF'
const x: number | string = 'hello';
// x is number | string โ annotation wins over inferred 'hello'
// x.toUpperCase(); // โ not on number
if (typeof x === 'string') {
x.toUpperCase(); // โ
narrowed
}
const user: { id: number; name: string } = {
id: 1,
name: 'Alice'
// extra: true // โ excess
};
console.log(x, user);
EOF
npx tsc --noEmit override.ts
# (no errors)
# ============================================
# PART 6: FULL EXAMPLE
# ============================================
cat > example.ts << 'EOF'
interface User {
id: number;
name: string;
nickname?: string;
}
function displayName(user: User): string {
return user.nickname ?? user.name;
}
const STATUS = {
Idle: 'idle',
Loading: 'loading',
Ready: 'ready'
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
const current: Status = STATUS.Idle;
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);
console.log(displayName({ id: 1, name: 'Alice' }));
console.log(doubled, current);
EOF
npx tsc --noEmit example.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc inference.ts contextual.ts annotate.ts as-const.ts override.ts example.ts
node inference.js
# [ Alice 30 true [ 'Alice', 'Bob' ] { id: 1, name: 'Alice' } hello hello 3 ]
node contextual.js
# [ [ 2, 4, 6 ] [ 2 ] [ '1', '2', '3' ] ]
node annotate.js
# [ done ]
# [ [ 'Alice' ] ]
node as-const.js
# [ Set to red ]
# [ [ 'red', 'green', 'blue' ] { mode: 'dark', retries: 3 } ]
node override.js
# [ hello { id: 1, name: 'Alice' } ]
node example.js
# [ Alice ]
# [ [ 2, 4, 6 ] idle ]
Quick Reference
Inference Sources
| Source | Example | Inferred |
|---|---|---|
| Initial value | const x = 5 | 5 (const) / number (let) |
| Function return | return a + b | number |
| Object literal | { a: 1 } | { a: number } |
| Array literal | [1, 2] | number[] |
| Callback param | .map(n => ...) | from expected type |
| Generic args | identity(x) | from arguments |
Widening Behavior
| Declaration | Type |
|---|---|
let x = 'a' | string |
const x = 'a' | 'a' |
let arr = ['a'] | string[] |
const arr = ['a'] | string[] |
const arr = ['a'] as const | readonly ['a'] |
let obj = { a: 1 } | { a: number } |
const obj = { a: 1 } as const | { readonly a: 1 } |
When to Annotate
| Situation | Annotate? |
|---|---|
| Function parameters | โ Always |
| Public return types | โ Usually |
| Empty array / object | โ |
| Class properties (no init) | โ |
| Function-typed variable | โ |
| Local variables with init | โ |
| Local helper returns | โ |
| Callback parameters in context | โ |
| Simple object literals | โ |
Annotation vs Inference
| Aspect | Annotation | Inference |
|---|---|---|
| Source | You write it | Compiler derives it |
| Boundary | โ Required | Rarely applies |
| Local code | Noise | Default |
| Literal preservation | Explicit | Only with const / as const |
| Excess checks | Enabled on object literals | Disabled |
| Error messages | Better (names the type) | Worse (expanded) |
as const Effects
| Before | After as const |
|---|---|
{ a: string } | { readonly a: 'a' } |
string[] | readonly ['a', 'b'] |
string | 'a' |
typeof Extraction
| Expression | Result |
|---|---|
typeof colors[number] | Element union |
typeof STATUS[keyof typeof STATUS] | Value union |
keyof typeof STATUS | Key union |
Contextual Typing Sources
| Source | Infers |
|---|---|
array.map(fn) | fn param type |
addEventListener | Event handler type |
| Variable annotation | Param type |
| Function parameter type | Param type |
| Generic argument | Type parameter |
When Not to Trust Inference
| Case | Why | Fix |
|---|---|---|
| Recursion | Self-reference | Annotate return |
async with json() | Returns any | Annotate return |
| Empty array | any[] | Annotate element |
Literal in let | Widens | as const or annotate |
| Object with optional fields | No ? inferred | Annotate shape |
Best Practices
โ Do This:
// Let inference handle locals
const name = 'Alice'; // โ
// Annotate function parameters
function greet(name: string): string { } // โ
// Annotate public returns
export function getUser(id: number): Promise<User> { } // โ
// Annotate empty containers
const items: Item[] = []; // โ
// Use `as const` for constants
const COLORS = ['red', 'green'] as const; // โ
// Rely on contextual typing for callbacks
nums.map(n => n * 2); // โ
// Annotate function-typed variables
const handler: (e: Event) => void = (e) => { }; // โ
// Extract from `as const` with `typeof`
type Color = typeof COLORS[number]; // โ
โ Don’t Do This:
// Don't over-annotate locals
const name: string = 'Alice'; // โ ๏ธ redundant
// Don't leave parameters unannotated
function greet(name) { } // โ implicit any
// Don't rely on inference for recursion
function fact(n: number) { return n * fact(n - 1); } // โ implicit any
// Don't trust `json()` inference
const user = await res.json(); // any // โ ๏ธ annotate
// Don't skip annotation on empty arrays
const items = []; // โ ๏ธ any[]
// Don't expect `as const` on mutable data
const cfg = { mode: 'dark' } as const; // โ ๏ธ if you mutate, don't
// Don't annotate the obvious
const x: number = 1 + 1; // โ ๏ธ redundant
// Don't use `any` because inference is unclear
const data: any = JSON.parse(s); // โ use unknown + validate
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Implicit any in parameters | No annotation | Annotate params |
Recursion inferred as any | Self-reference | Annotate return |
| Widened literal types | string not 'a' | as const or annotate |
Empty array any[] | No element type | Annotate |
json() returns any | No runtime check | Annotate + validate |
| Annotation overrides too narrowly | Unexpected type | Know what’s expected |
| Excess check missed | Via variable not literal | Assign literal directly |
as const on mutable data | Can’t reassign | Only for constants |
| Contextual typing missing | Standalone function | Annotate parameters |
| Union widening unexpected | Branches merge | Annotate if needed |
Real-World Examples
1. Inference for a constant
const name = 'Alice'; // string
2. Literal const
const role = 'admin'; // 'admin'
3. Widened let
let role = 'admin'; // string
4. Object inference
const user = { id: 1, name: 'A' };
// { id: number; name: string }
5. Annotated object
const user: User = { id: 1, name: 'A' };
6. Array inference
const nums = [1, 2, 3]; // number[]
7. Annotated empty array
const items: string[] = [];
8. Return inference
function add(a: number, b: number) { return a + b; }
9. Annotated return
function add(a: number, b: number): number { return a + b; }
10. Contextual callback
nums.map(n => n * 2);
11. Event handler
button.addEventListener('click', e => { /* e is MouseEvent */ });
12. as const object
const STATUS = { A: 'a', B: 'b' } as const;
13. as const array
const COLORS = ['red', 'green'] as const;
14. Extract union from array
type Color = typeof COLORS[number];
15. Extract value union
type Status = typeof STATUS[keyof typeof STATUS];
16. Annotated class property
class User { name: string; constructor(n: string) { this.name = n; } }
17. Annotated function variable
const log: (msg: string) => void = msg => console.log(msg);
18. Recursive with annotated return
function fact(n: number): number { return n <= 1 ? 1 : n * fact(n - 1); }
19. unknown instead of any
const data: unknown = JSON.parse(s);
20. Narrowed via annotation
const x: number | string = 'hello';
Visual: Inference Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ You write: โ
โ โ
โ const name = 'Alice'; โ
โ const age = 30; โ
โ const user = { id: 1, name }; โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Compiler infers: โ
โ โ
โ name: 'Alice' โ
โ age: 30 โ
โ user: { id: number; name: 'Alice' } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: let vs const Widening
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ let x = 'hello'; โ
โ โ
โ โ type: string โ
โ โ reassignable to any string โ
โ โ literal 'hello' lost โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const x = 'hello'; โ
โ โ
โ โ type: 'hello' โ
โ โ exact literal preserved โ
โ โ narrower, more precise โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const x = { a: 'hello' }; โ
โ โ
โ โ type: { a: string } โ
โ โ properties still widen โ
โ โ use `as const` for literal properties โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Contextual Typing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const nums = [1, 2, 3]; โ
โ โ
โ nums.map(n => n * 2) โ
โ โ โ
โ โ Compiler knows: โ
โ โ Array<number>.map(cb) โ
โ โ cb: (value: number) => U โ
โ โ โ
โ โผ โ
โ n is inferred as number โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ function f(n) { return n * 2; } โ
โ โ
โ No context โ n is implicit any โ
โ Annotate: function f(n: number) { } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Annotation vs Inference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Inference (default) โ
โ โ
โ const user = { id: 1, name: 'A' }; โ
โ โ { id: number; name: string } โ
โ โ
โ โข No boilerplate โ
โ โข Shape drifts with the literal โ
โ โข No excess checks โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Annotation (boundary) โ
โ โ
โ const user: User = { id: 1, name: 'A' }; โ
โ โ User โ
โ โ
โ โข Named type โ
โ โข Excess checks on literals โ
โ โข Stable contract โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: as const Transformations
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without `as const` โ
โ โ
โ const STATUS = { โ
โ Idle: 'idle', โ
โ Ready: 'ready' โ
โ }; โ
โ โ
โ โ { Idle: string; Ready: string } โ
โ โ mutable properties โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ With `as const` โ
โ โ
โ const STATUS = { โ
โ Idle: 'idle', โ
โ Ready: 'ready' โ
โ } as const; โ
โ โ
โ โ { readonly Idle: 'idle'; ... } โ
โ โ literal types preserved โ
โ โ can extract with typeof โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: When to Annotate โ Decision
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Is this a boundary? โ
โ โ
โ โโโ Parameter? โ annotate โ
โ โโโ Public return? โ annotate โ
โ โโโ Empty container? โ annotate โ
โ โโโ Function variable? โ annotate โ
โ โโโ Class property? โ annotate โ
โ โ โ
โ โโโ No โโโบ Is inference good? โ
โ โ โ
โ โโโ Yes โ rely on inference โ
โ โโโ No โ annotate โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Widening and Narrowing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Widening (default) โ
โ โ
โ 'hello' (literal) โ string โ
โ 42 (literal) โ number โ
โ ['a', 'b'] โ string[] โ
โ { a: 'x' } โ { a: string } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ `as const` โ no widening โ
โ โ
โ 'hello' as const โ 'hello' โ
โ ['a', 'b'] as const โ readonly ['a', 'b'] โ
โ { a: 'x' } as const โ { readonly a: 'x' } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Inference | Compiler derives the type |
| Annotation | You write the type |
| Widening | Literals โ general types |
let vs const | Widen vs preserve literals |
| Contextual typing | Params inferred from context |
as const | Preserve literals + readonly |
| Explicit return | Pins the contract |
| Type extraction | typeof X[number], keyof typeof X |
Key takeaways:
- TypeScript infers most types โ you don’t annotate every variable
letwidens literals to general types;constpreserves them- Object properties widen even under
constโ useas constto preserve literals - Function parameters are never inferred from usage โ they need annotation or context
- Contextual typing infers parameter types from where the function is used โ
.map, event handlers, etc. - Return types are inferred from the body โ except recursion, which requires annotation
- Annotate boundaries โ parameters, public returns, empty containers, function-typed variables, class properties
- Skip annotation for obvious locals, private helpers, callback parameters in context
- Annotations override inference โ the value is checked against your annotation
as constpreserves literal types and makes structures readonly โ for constant tables and configuration- Extract unions with
typeof X[number]andkeyof typeof X - Enable
strictto catch implicitanyโ the most important safety setting
Remember: Inference is the default and does most of the work. Annotations are for boundaries โ where the contract needs to be explicit, stable, and documented. let widens, const doesn’t, and as const opts out of widening entirely. Annotate parameters, public returns, and empty containers. Skip annotation where inference is clear and correct. That balance is what makes TypeScript both safe and ergonomic.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!