Tackling TypeScript
Please support this book: buy it or donate
(Ad, please don’t block.)

21 Type assertions (related to casting)



This chapter is about type assertions in TypeScript, which are related to type casts in other languages and performed via the as operator.

21.1 Type assertions

A type assertion lets us override a static type that TypeScript has computed for a value. That is useful for working around limitations of the type system.

Type assertions are related to type casts in other languages, but they don’t throw exceptions and don’t do anything at runtime (they do perform a few minimal checks statically).

const data: object = ['a', 'b', 'c']; // (A)

// @ts-expect-error: Property 'length' does not exist on type 'object'.
data.length; // (B)

assert.equal(
  (data as Array<string>).length, 3); // (C)

Comments:

Type assertions are a last resort and should be avoided as much as possible. They (temporarily) remove the safety net that the static type system normally gives us.

Note that, in line A, we also overrode TypeScript’s static type. But we did it via a type annotation. This way of overriding is much safer than type assertions because we are much more constrained: TypeScript’s type must be assignable to the type of the annotation.

21.1.1 Alternative syntax for type assertions

TypeScript has an alternative “angle-bracket” syntax for type assertions:

<Array<string>>data

I recommend avoiding this syntax. It has grown out of style and is not compatible with React JSX code (in .tsx files).

21.1.2 Example: asserting an interface

In order to access property .name of an arbitrary object obj, we temporarily change the static type of obj to Named (line A and line B).

interface Named {
  name: string;
}
function getName(obj: object): string {
  if (typeof (obj as Named).name === 'string') { // (A)
    return (obj as Named).name; // (B)
  }
  return '(Unnamed)';
}

21.1.3 Example: asserting an index signature

In the following code (line A), we use the type assertion as Dict, so that we can access the properties of a value whose inferred type is object. That is, we are overriding the static type object with the static type Dict.

type Dict = {[k:string]: any};

function getPropertyValue(dict: unknown, key: string): any {
  if (typeof dict === 'object' && dict !== null && key in dict) {
    // %inferred-type: object
    dict;

    // @ts-expect-error: Element implicitly has an 'any' type because
    // expression of type 'string' can't be used to index type '{}'.
    // [...]
    dict[key];
    
    return (dict as Dict)[key]; // (A)
  } else {
    throw new Error();
  }
}

21.2.1 Non-nullish assertion operator (postfix !)

If a value’s type is a union that includes the types undefined or null, the non-nullish assertion operator (or non-null assertion operator) removes these types from the union. We are telling TypeScript: “This value can’t be undefined or null.” As a consequence, we can perform operations that are prevented by the types of these two values – for example:

const theName = 'Jane' as (null | string);

// @ts-expect-error: Object is possibly 'null'.
theName.length;

assert.equal(
  theName!.length, 4); // OK
21.2.1.1 Example – Maps: .get() after .has()

After we use the Map method .has(), we know that a Map has a given key. Alas, the result of .get() does not reflect that knowledge, which is why we have to use the nullish assertion operator:

function getLength(strMap: Map<string, string>, key: string): number {
  if (strMap.has(key)) {
    // We are sure x is not undefined:
    const value = strMap.get(key)!; // (A)
    return value.length;
  }
  return -1;
}

We can avoid the nullish assertion operator whenever the values of a Map can’t be undefined. Then missing entries can be detected by checking if the result of .get() is undefined:

function getLength(strMap: Map<string, string>, key: string): number {
  // %inferred-type: string | undefined
  const value = strMap.get(key);
  if (value === undefined) { // (A)
    return -1;
  }

  // %inferred-type: string
  value;

  return value.length;
}

21.2.2 Definite assignment assertions

If strict property initialization is switched on, we occasionally need to tell TypeScript that we do initialize certain properties – even though it thinks we don’t.

This is an example where TypeScript complains even though it shouldn’t:

class Point1 {
  // @ts-expect-error: Property 'x' has no initializer and is not definitely
  // assigned in the constructor.
  x: number;

  // @ts-expect-error: Property 'y' has no initializer and is not definitely
  // assigned in the constructor.
  y: number;

  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}

The errors go away if we use definite assignment assertions (exclamation marks) in line A and line B:

class Point2 {
  x!: number; // (A)
  y!: number; // (B)
  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}