HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

28 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.

28.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 remove the safety net that the static type system normally gives us.

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

28.1.1 Obsolete alternative syntax for type assertions

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

<Array<string>>data

I recommend avoiding this syntax:

28.1.2 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]: unknown};

function getPropertyValue(dict: unknown, key: string): unknown {
  if (typeof dict === 'object' && dict !== null && key in dict) {
    assertType<object>(dict);

    // @ts-expect-error: 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();
  }
}

28.1.3 Example: as any

In the following example, the computed return type in line A does not match the value we return in line B:

type PrependDollarSign<Obj> = {
  [Key in (keyof Obj & string) as `$${Key}`]: Obj[Key]
};
function prependDollarSign<
  Obj extends object
>(obj: Obj): PrependDollarSign<Obj> { // (A)
  // @ts-expect-error: Type '{ [k: string]: any; }' is not assignable to
  // type 'PrependDollarSign<Obj>'.
  return Object.fromEntries( // (B)
    Object.entries(obj)
      .map(
        ([key, value]) => ['$'+key, value]
      )
  );
}

To make the error go away, we use as any in line A:

function prependDollarSign<
  Obj extends object
>(obj: Obj): PrependDollarSign<Obj> {
  return Object.fromEntries(
    Object.entries(obj)
      .map(
        ([key, value]) => ['$'+key, value]
      )
  ) as any; // (A)
}

const dollarObject = prependDollarSign({
  prop: 123,
});
assert.deepEqual(
  dollarObject,
  {
    $prop: 123,
  }
);
assertType<
  {
    $prop: number,
  }
>(dollarObject);

This is an extreme measure. Alas it’s unavoidable in this case. For more information, see “Computed return types of functions often don’t match returned values” (§33.14).

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

assert.equal(
  theName!.length, 4); // OK
28.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 {
  const value = strMap.get(key);
  assertType<string | undefined>(value);

  if (value === undefined) { // (A)
    return -1;
  }
  assertType<string>(value);

  return value.length;
}

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

28.2.3 Const assertions (as const)

Const assertions make values read-only and lead to more specific inferred types – e.g.:

const obj = { prop: 123 };
type _1 = Assert<Equal<
  typeof obj, { prop: number }
>>;
const constObj = { prop: 123 } as const;
type _2 = Assert<Equal<
  typeof constObj, { readonly prop: 123 }
>>;

const arr = ['a', 'b'];
type _3 = Assert<Equal<
  typeof arr, string[]
>>;
const constTuple = ['a', 'b'] as const;
type _4 = Assert<Equal<
  typeof constTuple, readonly ["a", "b"]
>>;

More information: “Const assertions (as const)” (§25.7).

28.2.4 satisfies operator

The satisfies operator enforces that a value has a given type but (mostly) otherwise does not affect the type of that value – e.g.:

const TextStyle  = {
  Bold: {
    html: 'b',
    latex: 'textbf',
  },
  // @ts-expect-error: Property 'latex' is missing in type
  // '{ html: string; }' but required in type 'TTextStyle'.
  Italics: { // (A)
    html: 'i',
  },
} satisfies Record<string, TTextStyle>; // (B)
type TTextStyle = {
  html: string,
  latex: string,
};

type TextStyleKeys = keyof typeof TextStyle; // (C)
type _ = Assert<Equal<
  TextStyleKeys, "Bold" | "Italics"
>>;

The satisfies operator in line B catches the error in line A but does not prevent TextStyle from having an object literal type. Therefore, we can extract the property keys in line C.

More information: “The satisfies operator” (§29).