Top Types & Bottom Types

In the previous chapter, we expanded our understanding of types with narrowing, widening, and immutability using const assertions. In this chapter, we will explore TypeScript’s top types (any and unknown), the bottom type (never), and the special type (void).

Top Types: any and unknown

The Flexibility of any

The any type is a universal type that can hold any value. While this provides flexibility, it disables type checking, which can lead to runtime errors. Here’s an example:

function logName(user: any) {
  console.log(user.props.name);
}
 
logName({ props: { name: 'Benny' } }); // Works
logName({}); // Crashes because 'props' is missing

The TypeScript team strongly advises against using any unless transitioning a JavaScript codebase to TypeScript. For safer alternatives, let’s explore unknown.

The Power of unknown

Introduced in TypeScript 3.0, unknown is a type-safe alternative to any. It requires developers to narrow down the type using control flow before accessing its properties:

function logName(user: unknown) {
  if (
    typeof user === 'object' &&
    user !== null &&
    'props' in user &&
    typeof user.props === 'object' &&
    'name' in user.props
  ) {
    const name = user.props.name;
    if (typeof name === 'string') {
      console.log(name);
    }
  }
}
 
logName({ props: { name: 'Benny' } }); // Logs "Benny"
logName({}); // Safely does nothing

By enforcing these checks, unknown ensures type safety and prevents runtime crashes.

Practical Use of unknown

In testing scenarios, mock objects often replace real implementations. Type assertions with unknown can help circumvent strict type checks temporarily:

import { jest } from '@jest/globals';
import { Client } from 'typesense';
 
const client = {
  health: {
    retrieve: jest.fn(),
  },
} as unknown as Client;

This approach strips type information with unknown before reasserting the desired type.

Bottom Type: never

The never type represents values that never occur. It’s typically used for unreachable code or exhaustive checks in TypeScript:

Example:

type StreamStatus = 'ONLINE' | 'OFFLINE';
 
function handleResponse(status: StreamStatus): void {
  switch (status) {
    case 'ONLINE':
      console.log('Stream is online.');
      break;
    case 'OFFLINE':
      console.log('Stream is offline.');
      break;
    default:
      const neverStatus: never = status;
      throw new Error(`Unhandled status: ${neverStatus}`);
  }
}

By leveraging never in the default case, TypeScript ensures that all possible cases are handled. Adding a new status, like "IDLE", would trigger a compile-time error until it is explicitly handled.

No Type: void

The void type represents the absence of a return value. It is commonly used for functions that perform actions without returning anything:

function logMessage(message: string): void {
  console.log(message);
}

Comparison: void and undefined

While void and undefined may seem similar, they have distinct purposes. A function with an undefined return type must explicitly return undefined, while a void function does not:

function doNothing(): undefined {
  return undefined;
}
 
function logNothing(): void {
  // No return statement needed
}

In scenarios where the return value is irrelevant, void is preferred to communicate the lack of interest in the return type.


What You Have Learned

Top Types: any and unknown: You have learned about TypeScript’s top types, any and unknown. The any type allows a variable to hold any value to mimic JavaScript’s standard behaviour, potentially causing runtime errors. The unknown type, introduced in TypeScript 3.0, provides a safer alternative by requiring explicit type narrowing before accessing properties, ensuring type safety.

Bottom Type: never: You now understand that the never type represents values that never occur. It’s useful for situations like unreachable code or exhaustive checks, ensuring that all possible cases are handled in TypeScript.

No Type: void: You’ve learned that the void type is used for functions that do not return a value. It signals the absence of a return value, distinguishing it from undefined, which requires explicit returning of undefined. The void type is useful in your code to indicate that the return value is not important or relevant.


Quiz

  1. Which statement best describes the any type in TypeScript?
  • a) It allows you to assign only string and number values.
  • b) It is the safest type for any unknown data structure.
  • c) It can hold any value and was designed to mimic JavaScript's original type behaviour.
  • d) It restricts object properties to a known subset.
  1. What must you do before accessing properties on a variable typed as unknown?
  • a) Assign it to an intermediate variable of type any.
  • b) Perform type checks or narrowing to confirm its structure.
  • c) Cast it to string and then access properties.
  • d) Nothing, unknown behaves exactly like any.
  1. How is the never type typically used in TypeScript?
  • a) For return values of functions that never return.
  • b) For variables that can accept any type of value.
  • c) For optional properties in interfaces.
  • d) As a shorthand for undefined in TypeScript.
  1. What does TypeScript enforce if you include a never type in a switch statement’s default case?
  • a) It allows partial coverage of cases without error.
  • b) It automatically converts all other cases to any.
  • c) It ensures all possible cases are exhaustively handled.
  • d) It disables type checking within the switch block.
  1. Which statement correctly explains the difference between void and undefined in a function return type?
  • a) A function returning void must explicitly return undefined.
  • b) Both void and undefined require a return statement.
  • c) Using void indicates no meaningful return value; undefined means the function must explicitly return undefined.
  • d) void is only valid in strict mode, whereas undefined is not.
  1. Which approach ensures runtime immutability for an object in JavaScript in addition to TypeScript compile-time checks?
  • a) Declaring the object with unknown.
  • b) Using as const in TypeScript, which automatically freezes the object at runtime.
  • c) Calling Object.freeze() on the object in strict mode.
  • d) Specifying the object’s type as never to prevent reassignment.