Types over Values
Lookup sheet for stage 2. The question it exists to answer: what does this annotation promise, and where does the compiler let it slide on purpose?
The annotations
| Write | For |
|---|---|
string, number, boolean, null, undefined, symbol, bigint | a primitive |
string[] or Array<string> | an array, same type either spelling |
[number, string] | a tuple: fixed length, one type per position |
{ name: string; age: number } | an object shape |
age?: number | optional: key may be absent |
age: number \| undefined | required key, value may be undefined |
(x: string) => void | a function value |
string \| number | a union: genuinely either |
"low" \| "medium" \| "high" | a union of literals |
any | switches checking off; a defect, not a convenience |
What the compiler checks unasked
TypeScript 7 type-checks strictly with no configuration file. Five flags, including noImplicitAny and strictFunctionTypes, default to on unless strict is explicitly false. See lesson 8.
Two strict-shaped checks are still off by default:
| Flag | Would catch | Taught in |
|---|---|---|
noUncheckedIndexedAccess | an out-of-bounds read typed without undefined | lesson 14, named |
exactOptionalPropertyTypes | a ? property assigned undefined | lesson 8, gap only, unnamed |
Widening
| Expression | Inferred | Keep the literal by |
|---|---|---|
let s = "hello"; | string | const instead of let, or annotate the literal type directly |
const s = "hello"; | "hello" | already literal |
const o = { kind: "a" }; | o.kind is string | as const on the object literal |
const o = { kind: "a" } as const; | o.kind is "a", o is deeply readonly | already literal |
let widens because it might be reassigned; a property widens the same way even inside a const, since the property itself stays mutable. See lesson 10.
Assignability verdicts
| Source | Target | Verdict | Why |
|---|---|---|---|
{ a: 1, b: 2 }, a fresh literal | { a: number } | TS2353 | excess property check, fresh literal only; a real check |
| same value routed through a variable | { a: number } | compiles | structural assignability: "at least" the members needed |
a readonly object | same shape, no readonly | compiles | readonly not part of object-type assignability; deliberate hole |
() => 42 | () => void | compiles | void means ignored, not "nothing returned"; deliberate hole |
(x: string) => void, as a function-type property | (x: string \| number) => void | TS2322 | contravariant, under strictFunctionTypes; a real check |
| the identical narrower parameter, as a method | a method signature of the wider type | compiles | method parameters checked bivariantly; deliberate exemption |
Sorted by the last column rather than the middle one. Four of the six compile, and only structural assignability is the compiler making a promise; the other three are places it has agreed not to look. That is the difference worth carrying away, because "it compiles" reads the same in all four cases and means something different in three of them.
See lesson 9, lesson 11 and lesson 13.
Narrowing
| Operator | Narrows on |
|---|---|
typeof x === "..." | the primitive tag |
x instanceof C | C.prototype in the chain |
"k" in x | property presence, not a runtime tag |
x === literalValue | one member of a literal union |
truthiness, if (x) | every falsy value at once: false, 0, -0, 0n, "", null, undefined, NaN |
early return or throw | the rest of the function |
| Binding | Lost when | Survives |
|---|---|---|
| local variable | reassigned anywhere later in the function, even after a closure captures it | never reassigned in the function |
object property (this.x) | the read is deferred inside a closure | an arbitrary function call in between |
See lesson 12.
Tuples
| Operation | Checked against the declared length |
|---|---|
| index read within the length | yes, typed to that position |
| index read past the length | yes, TS2493 |
| destructure past the length | yes, TS2493 |
.length | yes, the literal count, not number |
.push, .pop, .splice | no; array methods, never wired to the tuple's length |
See lesson 9.
Annotate or not
- Parameter: always, since there is no initialiser to infer from.
- Exported function's return type: annotate, since the signature is a boundary other code depends on.
- Local variable with an initialiser: leave inferred, unless the inferred type is wider than the one you mean to keep.
- Empty collection filled outside one scope, or a return type you want checked against intent rather than the body: annotate.
- Callback parameter already pinned by a declared function type at the call site: leave bare.
Diagnostics seen in this stage
TS number | Meaning | Usual cause |
|---|---|---|
TS7006 | parameter implicitly any | bare parameter, no contextual type |
TS2322 | not assignable to the target type | widened literal, unhandled null/undefined, lost narrowing, narrower callback property |
TS2741 | required property missing | object literal omits a property the target requires |
TS2339 | property does not exist | method missing from a union member, or from a readonly array |
TS2493 | tuple index outside the declared length | reading or destructuring past the fixed length |
TS2540 | cannot assign to a readonly property | writing through the readonly-typed name itself |
TS2353 | excess property in an object literal | a fresh literal carries a key the target never declared |
TS2345 | argument not assignable to the parameter | widened or mismatched argument |
TS18047 | possibly null | union with null, unnarrowed |
TS18048 | possibly undefined | union with undefined, unnarrowed |
TS18049 | possibly null or undefined | union with both, unnarrowed |