Skip to content
academia.sh

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

  • any turns off type checking; being assignable in both directions, it carries no guarantee and spreads the lack of checking.
  • Implicit any is an omission and is reported by the noImplicitAny option; explicit any is a decision.
  • unknown is 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 unknown and enters the model only after being checked field by field; the as notation does not substitute for this check.
  • never is the bottom type: it has no values, it is assignable to every type; it is used for exhaustiveness checking in switch blocks.

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.

Start typing to search.

↑↓ Esc navigate · open · close