Lesson 03 / 23
any, unknown, and never
The assignability rules and use sites of any, which turns off checking, unknown, which forces it, and never, which has no values.
Contents
The previous lesson wrote the measurement record’s first typed version, but the record
was still a hand-written object. Real data comes from a file or the network and comes
out of a JSON.parse call. What type is that call’s return value?
The answer introduces the type system’s two endpoints. The any type says “this could
be anything, do not check”; the unknown type says “this could be anything, so check
first.” The difference between the two is one of the most important decisions for a
record’s safety. At the end of the lesson comes a third endpoint: the never type,
which has no values at all.
any: Turning Off Checking
any tells the type checker to assert nothing about a value:
const raw: any = JSON.parse('{"id":"s-01","value":"23.0"}'); console.log(raw.value.toFixed(1)); console.log(raw.missing.field); const num: number = raw;
This file passes type checking without error. All three lines are wrong: the
value field is a string, there is no field called missing, and raw is not a
number. The compiler reports none of them, because it has no assertion about which
fields an any value has.
any behaves in both directions: a value of type any can be assigned to any type,
and any type can be assigned to any. This takes the type out of the checking
network. Once a field is any, every expression derived from it also stays unchecked
— the lack of checking spreads.
The declared return type of the JSON.parse function is any. The reason is clear:
the content of the string is only known at runtime, the compiler has nothing to say.
The result resembles what was seen in the first lesson — type checking falls silent,
the error is deferred to runtime.
The compiler can also refuse to infer any when no type is written:
function average(measurements) { return measurements.length; } console.log(average([1, 2, 3]));
c6.ts(1,18): error TS7006: Parameter 'measurements' implicitly has an 'any' type.
This diagnostic comes from the noImplicitAny option and is part of the strict
configuration. The distinction has to be seen: implicit any is an omission, the
compiler warns; explicit any is a decision, the compiler stays silent. Searching
a codebase for any notations means searching for where checking was turned off.
unknown: Deferring Checking
unknown covers the same value set as any — every value can be assigned to type
unknown — but the reverse direction is closed:
const raw: unknown = JSON.parse('{"id":"s-01","value":"23.0"}'); console.log(raw.value); const num: number = raw;
c2.ts(3,13): error TS18046: 'raw' is of type 'unknown'. c2.ts(4,7): error TS2322: Type 'unknown' is not assignable to type 'number'.
Nothing can be done with a value of type unknown: its fields cannot be
accessed, it cannot be called, it cannot be assigned to another type. The only thing
that can be done is narrowing its type.
The relationship between the two can be described with an ordering. In the direction
of assignability, unknown sits at the top: every type is assignable to it, it is
assignable to none. This is why it is called the top type. any sits outside
this ordering; assignable in both directions, and precisely for that reason it
carries no guarantee.
Practical rule: unknown is used at the system boundary. External data is
received as unknown, checked, and only enters the model after that check.
Validation at the Boundary
Reading the measurement record from a file is an application of this rule:
type Measurement = { id: string; value: number }; function read(raw: string): Measurement | null { const parsed: unknown = JSON.parse(raw); if ( typeof parsed === "object" && parsed !== null && "id" in parsed && typeof parsed.id === "string" && "value" in parsed && typeof parsed.value === "number" ) { return { id: parsed.id, value: parsed.value }; } return null; } console.log(read('{"id":"s-01","value":21.4}')); console.log(read('{"id":"s-01","value":"23.0"}')); console.log(read('42'));
Output:
{ id: 's-01', value: 21.4 }
null
null
Every part of the condition does one step of narrowing. After typeof parsed === "object", the compiler knows the value is an object; parsed !== null eliminates the
possibility of null; "id" in parsed proves that field exists; the typeof check
determines the field’s type. By the time the body of the condition is entered, the
compiler has proven that parsed.id is a string — not on the programmer’s word,
but based on the checks in the code.
This is the course’s axis: the type system is a layer of proof, and checks are the steps of the proof. The rules of narrowing and less repetitive ways to write it are covered in the Type Narrowing lesson.
The same job could also have been done by writing as Measurement, and it would have
misbehaved as seen in the first lesson. The difference is this: with as, the
guarantee comes from the programmer’s assertion; in the code above, it comes from
checks that actually run.
never: The Empty Type
never is the type with no values. It is the empty set. It shows up in two places.
First, as the return type of functions that never return by the normal path:
function halt(message: string): never { throw new Error(message); } const anything: string = halt("record could not be read"); const nothing: never = "s-01"; console.log(anything, nothing);
c5.ts(6,7): error TS2322: Type '"s-01"' is not assignable to type 'never'.
The fifth line does not error, the sixth does. The reason is never’s place in the
assignability ordering: never can be assigned to every type, no type can be assigned
to never — much like the empty set being a subset of every set. This is why never
is called the bottom type. Because the halt function returns no value at all,
claiming that the value it returns has to be string is a vacuous claim, and the
compiler allows it.
The second appearance is more useful: exhaustiveness checking. Let the measurement record’s states be defined with a union:
type Status = "valid" | "suspect" | "invalid"; function label(status: Status): string { switch (status) { case "valid": return "measurement valid"; case "suspect": return "measurement suspect"; default: { const unhandled: never = status; return unhandled; } } } console.log(label("valid"));
c4.ts(10,13): error TS2322: Type '"invalid"' is not assignable to type 'never'.
The diagnostic names the unhandled case directly. The mechanism is this: after the two
case branches, the compiler knows only "invalid" can reach the default branch.
Attempting to assign to never tests whether the remaining set is empty. If the set is
not empty, a diagnostic appears, and the value inside it is written into the
diagnostic’s text.
This pattern’s value shows up when a new case is added to the union. If a fourth value
is added to the Status type, every switch block that does not handle that value
errors at compile time. The type system becomes a maintenance tool here: changing the
model forces a review of all code that uses it.
Where the Three Types Sit
The relationship among the three can be gathered into a single table:
| Type | What can be assigned to it | Where an assignment from it can go | Where used |
|---|---|---|---|
any |
Every type | Every type | Temporary, during gradual migration |
unknown |
Every type | Nowhere (narrowing required) | System boundary |
never |
Nowhere | Every type | Exhaustiveness checking, non-returning functions |
The table’s first row shows why any is not a type but a gate: it is the only row
with both columns filled, and that means it imposes no constraint at all. unknown
and never are each other’s opposite — one covers everything and allows nothing, the
other covers nothing and goes everywhere.
any is not banned outright. As will be seen in the JavaScript Interop lesson, it is
temporarily necessary during gradual migration of a codebase with no type
information. The rule is: any is a transition marker, not a solution; where
it remains is documented and narrowed.
Summary
anyturns off type checking; being assignable in both directions, it carries no guarantee and spreads the lack of checking.- Implicit
anyis an omission and is reported by thenoImplicitAnyoption; explicitanyis a decision. unknownis the top type: every value is assignable to it, it is assignable to no type; it has to be narrowed before use.- Data from a system boundary is received as
unknownand enters the model only after being checked field by field; theasnotation does not substitute for this check. neveris the bottom type: it has no values, it is assignable to every type; it is used for exhaustiveness checking inswitchblocks.
Next Step
Up to here, types were written by hand. Yet in most of the examples above, the
compiler already knew types that were never written: the narrowed types inside read,
the object literal’s field types, the type of array elements. The next lesson covers
where this knowledge comes from — the rules and limits of type inference.
To keep your progress and take notes, Log in
My notes
Log in to take notes.