The top types any and unknown in TypeScript

[2020-06-03] dev, javascript, typescript
(Ad, please don’t block)

In TypeScript, any and unknown are types that contain all values. In this blog post, we examine how they work.

TypeScript’s two top types  

any and unknown are so-called top types in TypeScript. Quoting Wikipedia:

The top type [...] is the universal type, sometimes called the universal supertype as all other types in any given type system are subtypes [...]. In most cases it is the type which contains every possible [value] in the type system of interest.

That is, when viewing types as sets of values (for more information on what types are, see “What is a type in TypeScript? Two perspectives”), any and unknown are sets that contain all values. As an aside, TypeScript also has the bottom type never, which is the empty set.

The top type any  

If a value has type any, we can do everything with it:

function func(value: any) {
  // Only allowed for numbers, but they are a subtype of `any`
  5 * value;

  // Normally the type signature of `value` must contain .propName
  value.propName;

  // Normally only allowed for Arrays and types with index signatures
  value[123];
}

Every type is assignable to type any:

let storageLocation: any;

storageLocation = null;
storageLocation = true;
storageLocation = {};

Type any is assignable to every type:

function func(value: any) {
  const a: null = value;
  const b: boolean = value;
  const c: object = value;
}

With any we lose any protection that is normally given to us by TypeScript’s static type system. Therefore, it should only be used as a last resort, if we can’t use more specific types or unknown.

Example: JSON.parse()  

The result of JSON.parse() depends on dynamic input, which is why the return type is any (I have omitted the parameter reviver from the signature):

JSON.parse(text: string): any;

JSON.parse() was added to TypeScript before the type unknown existed. Otherwise, its return type would probably be unknown.

Example: String()  

The function String(), which converts arbitrary values to strings, has the following type signature:

interface StringConstructor {
  (value?: any): string; // call signature
  // ···
}

The top type unknown  

The type unknown is a type-safe version of the type any. Whenever you are thinking of using any, try using unknown first.

Where any allows us to do anything, unknown is much more restrictive.

Before we can perform any operation on values of type unknown, we must first narrow their types via:

  • Type assertions:

    function func(value: unknown) {
      // @ts-ignore: Object is of type 'unknown'.
      value.toFixed(2);
    
      // Type assertion:
      (value as number).toFixed(2); // OK
    }
    
  • Equality:

    function func(value: unknown) {
      // @ts-ignore: Object is of type 'unknown'.
      value * 5;
    
      if (value === 123) { // equality
        // %inferred-type: 123
        value;
    
        value * 5; // OK
      }
    }
    
  • Type guards:

    function func(value: unknown) {
      // @ts-ignore: Object is of type 'unknown'.
      value.length;
    
      if (typeof value === 'string') { // type guard
        // %inferred-type: string
        value;
    
        value.length; // OK
      }
    }
    
  • Assertion functions:

    function func(value: unknown) {
      // @ts-ignore: Object is of type 'unknown'.
      value.test('abc');
    
      assertionFunction(value);
    
      // %inferred-type: RegExp
      value;
    
      value.test('abc'); // OK
    }
    
    function assertionFunction(arg: unknown): asserts arg is RegExp {
      if (! (arg instanceof RegExp)) {
        throw new TypeError('Not a RegExp: ' + arg);
      }
    }