Type assertions in TypeScript

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

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

Type assertions  

A type assertion lets us override a static type that TypeScript has computed for a storage location. 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-ignore: Property 'length' does not exist on type 'object'.
data.length; // (B)

assert.equal(
  (data as Array<string>).length, 3); // (C)
  • In line A, we widen the type of the Array to object.

  • In line B, we see that this type doesn’t let us access any properties (details).

  • In line C, we use a type assertion (the operator as) to tell TypeScript that data is an Array. Now we can access property .length.

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 there is much less you can do. TypeScript’s type must be assignable to the type of the annotation.

Alternative syntax for type assertions  

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

<Array<string>>data

That syntax has grown out of style and is not compatible with React JSX code (in .tsx files).

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)';
}

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 inferred 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-ignore: Element implicitly has an 'any' type because
    // expression of type 'string' can't be used to index type '{}'.
    //   No index signature with a parameter of type 'string' was
    //   found on type '{}'.
    dict[key];
    
    return (dict as Dict)[key]; // (A)
  } else {
    throw new Error();
  }
}

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-ignore: Object is possibly 'null'.
theName.length;

assert.equal(
  theName!.length, 4); // OK

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;
}

Since the values of strMap are never undefined, we can detect missing Map entries by checking if the result of .get() is undefined (line A):

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;
}

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-ignore: Property 'x' has no initializer and is not definitely
  // assigned in the constructor.
  x: number;

  // @ts-ignore: 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;
  }
}