understand-type-widening

📁 marius-townhouse/effective-typescript-skills 📅 10 days ago
1
总安装量
1
周安装量
#54677
全站排名
安装命令
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill understand-type-widening

Agent 安装分布

mcpjam 1
openhands 1
replit 1
windsurf 1
zencoder 1

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 string instead of "specific-value"
  • const and let give 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:

  • let variables widen literals to their base type
  • const variables keep literal types (for primitives)
  • Object/array contents widen even with const
  • Use as const for 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:

  1. Specificity – Catch real bugs with narrow types
  2. 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 string not "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.