understand-type-widening
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill understand-type-widening
Agent 安装分布
Skill 文档
Understand Type Widening
Overview
When TypeScript infers a type from a value, it often widens it.
A variable initialized with "x" could be intended to hold any string, or just the literal "x". TypeScript guesses using heuristics, and understanding these helps you write predictable code.
When to Use This Skill
- Confused why a type is
stringinstead of"specific-value" constandletgive different types for the same value- Array literals get unexpected element types
- Object properties are wider than expected
- Type errors about literals not being assignable
The Iron Rule
ALWAYS understand how your declaration style affects inferred types.
Remember:
letvariables widen literals to their base typeconstvariables keep literal types (for primitives)- Object/array contents widen even with
const - Use
as constfor full literal inference
Detection: The Widening Surprise
If TypeScript infers a broader type than you expected, you’re seeing widening.
// Primitive widening with let
let x = 'x';
// ^? let x: string (not "x")
// No widening with const (for primitives)
const y = 'y';
// ^? const y: "y"
// But object properties still widen
const obj = { x: 1, y: 2 };
// ^? const obj: { x: number; y: number } (not { x: 1, y: 2 })
Why Widening Exists
TypeScript must balance two goals:
- Specificity – Catch real bugs with narrow types
- Flexibility – Allow reasonable mutations
// Without widening, this would fail:
let x = 'x'; // If inferred as "x", then...
x = 'y'; // Error! "y" is not assignable to "x"
// With widening:
let x = 'x'; // Inferred as string
x = 'y'; // OK
The Widening Rules
Rule 1: let Widens, const Preserves (Primitives)
// let â widened
let a = 'hello';
// ^? let a: string
let b = 42;
// ^? let b: number
let c = true;
// ^? let c: boolean
// const â literal
const d = 'hello';
// ^? const d: "hello"
const e = 42;
// ^? const e: 42
const f = true;
// ^? const f: true
Rule 2: Object Properties Always Widen
const point = { x: 3, y: 4 };
// ^? const point: { x: number; y: number }
// Not { x: 3, y: 4 } - because you might do:
point.x = 10; // This must be valid
Rule 3: Array Elements Widen
const arr = [1, 2, 3];
// ^? const arr: number[]
// Not [1, 2, 3] or readonly [1, 2, 3]
Rule 4: Mixed Arrays Get Union Types
const mixed = [1, 'x'];
// ^? const mixed: (string | number)[]
// TypeScript picks the "best common type"
Controlling Widening
Use as const for Full Literal Inference
// Regular object - properties widen
const obj1 = { x: 1, y: 2 };
// ^? const obj1: { x: number; y: number }
// With as const - properties are literal and readonly
const obj2 = { x: 1, y: 2 } as const;
// ^? const obj2: { readonly x: 1; readonly y: 2 }
// Arrays too
const arr = [1, 2, 3] as const;
// ^? const arr: readonly [1, 2, 3]
Use Type Annotations to Be Explicit
// Annotate to get the type you want
const x: 'x' | 'y' = 'x';
// ^? const x: "x" | "y"
// Can be reassigned to 'y', but nothing else
Use Satisfies for Checked Inference
type Point = { x: number; y: number };
// Type annotation: loses literal types
const p1: Point = { x: 1, y: 2 };
// ^? const p1: Point
// satisfies: keeps literals while checking structure
const p2 = { x: 1, y: 2 } satisfies Point;
// ^? const p2: { x: number; y: number }
p2.x; // number (not 1, but still good for inference)
Common Widening Problems
Problem: Literal Expected, Got String
type HTTPMethod = 'GET' | 'POST' | 'PUT';
function makeRequest(method: HTTPMethod) { /* ... */ }
let method = 'GET';
makeRequest(method);
// ~~~~~~ Argument of type 'string' is not assignable
Solutions:
// Solution 1: Use const
const method = 'GET';
makeRequest(method); // OK
// Solution 2: Type annotation
let method: HTTPMethod = 'GET';
makeRequest(method); // OK
// Solution 3: as const
let method = 'GET' as const;
makeRequest(method); // OK
Problem: Object Property Too Wide
type Config = {
mode: 'development' | 'production';
debug: boolean;
};
function configure(config: Config) { /* ... */ }
const config = { mode: 'development', debug: true };
configure(config);
// ~~~~~~ Type 'string' is not assignable to type '"development" | "production"'
Solutions:
// Solution 1: Type annotation
const config: Config = { mode: 'development', debug: true };
// Solution 2: as const on the whole object
const config = { mode: 'development', debug: true } as const;
// Solution 3: as const on just the property
const config = { mode: 'development' as const, debug: true };
Problem: Tuple Becomes Array
function setPoint(point: [number, number]) { /* ... */ }
const coords = [10, 20];
setPoint(coords);
// ~~~~~~ Type 'number[]' is not assignable to type '[number, number]'
Solutions:
// Solution 1: Type annotation
const coords: [number, number] = [10, 20];
// Solution 2: as const (makes it readonly)
const coords = [10, 20] as const;
// Note: readonly [10, 20] may not be assignable to [number, number]
// depending on the function's type
Pressure Resistance Protocol
1. “Just Use any“
Pressure: “Type is wrong, just cast it to any“
Response: The type is right, just wider than you want.
Action: Use const, type annotations, or as const.
2. “TypeScript Is Being Dumb”
Pressure: “It’s obviously ‘GET’, why infer string?”
Response: TypeScript assumes let variables will change.
Action: Use const for values that won’t change.
Red Flags – STOP and Reconsider
- Casting to fix literal type errors
- Surprised that object properties are
stringnot"specific" - Tuple types becoming arrays
- Union literals widening unexpectedly
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| “const should make it literal” | Only for primitives, not object contents |
| “The value is clearly X” | let means it could change |
| “as any fixes it” | Use proper narrowing controls |
Quick Reference
| Declaration | Value | Inferred Type |
|---|---|---|
let x = "hello" |
string literal | string |
const x = "hello" |
string literal | "hello" |
const x = { a: 1 } |
object | { a: number } |
const x = { a: 1 } as const |
object | { readonly a: 1 } |
const x = [1, 2] |
array | number[] |
const x = [1, 2] as const |
array | readonly [1, 2] |
The Bottom Line
TypeScript widens types to allow reasonable mutations.
Use const for primitives, type annotations for explicit types, and as const when you need full literal inference. Understanding widening prevents surprising type errors and helps you write more precise types.
Reference
Based on “Effective TypeScript” by Dan Vanderkam, Item 20: Understand How Variables Get Their Types.