Skip to content

Exploring Extreme Types in TypeScript. Top and Bottom Types

Posted on:July 1, 2024

In TypeScript, the concept of extreme types refers to types that accept a wide range of values or none at all. These are known as top and bottom types, respectively. Understanding these types and their applications can significantly improve your TypeScript coding practices. This article will delve into the specifics of these types and demonstrate practical use cases, particularly focusing on the never type and its role in exhaustive conditionals.

Top types are those that can accept any value. In TypeScript, any and unknown are two primary examples of top types.

Bottom types are those that cannot hold any value. The never type in TypeScript is a bottom type.

Table of contents

Open Table of contents

1. The any Type

The any type in TypeScript can hold any value. This flexibility can be useful, especially when transitioning a JavaScript codebase to TypeScript, where many variables may initially be typed as any.

let something: any;
something = 42;
something = "Hello";
something = window.document;
something = setTimeout;

In this example, something starts as a number, then becomes a string, then a Document object, and finally a function. TypeScript allows these assignments without complaints because any disables type checking.

While any provides flexibility, it should be used judiciously.

let something: any = 42;
something.is.not.here
          ^?any - TypeScript does not stop you, but this will fail at runtime

vs

let something: number = 42;
something.is.not.here
          ^?Property 'is' does not exist on type 'number'.

2. The unknown Type

The unknown type is similar to any in that it can hold any value. However, it requires you to perform type checks before using the value, adding a layer of type safety.

let value: unknown;
value = 42;
value = "Hello";

if (typeof value === "string") {
  console.log(value.toUpperCase()); // Safe to use as a string
} else if (typeof value === "number") {
  console.log(value.toFixed(2)); // Safe to use as a number
}

With unknown, TypeScript ensures you can’t use the variable until you’ve verified its type, preventing potential runtime errors.

3. The never Type

The never type represents values that never occur. It is often used to indicate functions that never return or to perform exhaustive type checking.

function error(message: string): never {
  throw new Error(message);
}

Here, the function error always throws an error and never returns a value, making its return type never.

One of the most powerful patterns using the never type is exhaustive conditionals. This technique ensures all possible cases in a union type are handled.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

In this example, the default case in the switch statement ensures that all possible Shape types are handled. If a new type is added to the Shape union without updating the area function, TypeScript will raise a type error, prompting you to handle the new case.

4. Bonus: Almost-Top Types

In TypeScript, the object type and its closely related types can often be confusing due to the overloaded terminology and their subtle differences. Let’s dive into what these types are, how they differ, and their practical uses.

The object Type

The object type in TypeScript represents all possible values except for the primitive types: number, string, boolean, null, undefined, symbol, and bigint. This means that object includes:

•	Plain objects ({})
•	Arrays ([])
•	Functions (function() {})
•	Instances of classes
let val: object;
val = { status: "success" }; // valid
val = function () {}; // valid
val = "string"; // invalid
val = null; // invalid

The Empty Object Type {}

The {} type, known as the empty object type, is another almost-top type in TypeScript. It is more accepting than the object type because it accepts all possible values except null and undefined.

let val2: {};
val2 = 4; // valid
val2 = "string"; // valid
val2 = new Date(); // valid
val2 = null; // invalid
val2 = undefined; // invalid

The empty object type {} can be used to remove null and undefined from a type. This is particularly useful for ensuring that a value is neither null nor undefined without performing explicit null checks.

type NullableStringOrNumber = string | number | null | undefined;
type NonNullableStringOrNumber = NullableStringOrNumber & {};

let val3: NonNullableStringOrNumber;
val3 = "hello"; // valid
val3 = 42; // valid
val3 = null; // invalid
val3 = undefined; // invalid

TypeScript provides a built-in utility type called NonNullable that achieves the same effect as intersecting with {}.

type AnyType = string | number | null | undefined;
type IntersectedWithAny = AnyType & any;

let val5: IntersectedWithAny;
val5 = "hello"; // valid
val5 = 42; // valid
val5 = null; // valid
val5 = undefined; // valid

Conclusion

Understanding and utilizing top and bottom types in TypeScript can significantly enhance your code’s flexibility and safety. While any and unknown provide ways to handle a wide range of values, never ensures exhaustive checks and helps catch potential errors during development. By mastering these types, you can write more robust and maintainable TypeScript code.