Web dev and stuff GitHub Twitter

Typescript assertions

The below implies that you have already read the type assertions section in typescript documentation.

List of content

as cast operator

Added in v1.6

This operator allows you to explicitly cast the type for a value of a different type without typescript raising an error. This is a dangerous a foot gun like feature that should be used with care and consciously. To quote the typescript docs this is a way to tell the compiler trust me, I know what I’m doing.

This feature has two ways to use it.

const a = notFoo as Foo;
/**
 * The same thing
 * but wont work for `.tsx` files
 */
const b = <Foo>notFoo;

/**
 * now both `a` and `b` are of type `Foo`
 */

Const assertions

const foo = {} as const

Added in v3.4

This feature allows you to disable type widening when declaring values in typescript.

const plainObj = {a: 1, b: 'foo'};

plainObj; // {a: number; b: string}

const constObj = {a: 1, b: 'foo'} as const;
const constObjAlt = <const>{a: 1, b: 'foo'};

constObj; // {readonly a: 1; readonly b: 'foo'}

This is not the same as using Object.freeze

const constObj = {a: 1, b: 'foo', c: {d: 'bar'}} as const;

constObj; // {readonly a: 1, readonly b: 'foo', readonly c: {readonly d: 'bar}}

// @ts-expect-error Cannot assign to 'd' because it is a read-only property.
constObj.c.d = 'foo'

const frozen = Object.freeze({a: 1, b: 'foo', c: {d: 'bar'}})

frozen; // Readonly<{a: number; b: string; c: {d: string}}>

// @ts-expect-error Cannot assign to 'b' because it is a read-only property.
frozen.b = 'foo 2'

// no error since `Readonly` is not deep
frozen.c.d = 'foo'

The key things that happen when const assertions are being used are:

Assert functions

Added in v3.7

Assert functions are similar to type guards with the only difference that the function throws instead of returning a falsy value. This works on par with nodejs assert module.

Using assert function you can validate an input ie

function plainAssertion(arg: unknown): asserts arg {
    if (!arg) {
        throw new Error(`arg is expected to be truthy, got "${arg}"`);
    }
}

function foo(input: boolean, item: string | null) {
    input; // boolean
    plainAssertion(input);
    input; // true

    item; // string | null
    plainAssertion(item);
    item; // string
}

Alternatively you can narrow down the type to be more specific. This is when the similarity with type guards shows.

function specificAssertion(arg: unknown): asserts arg is string {
    if (typeof arg !== 'string') {
        throw new Error(`arg is expected to be string, got "${arg}"`)
    }
}

function bar(input: string | null) {
    input; // string | null
    specificAssertion(input);
    input; // string
}

Key remapping in mapped types

ie {[K in keyof T as Foo]: T[K]}

Added in v4.1

Reminder of mapped type syntax in typescript

type Inlined = {
    [key in 'a' | 'b' | 'c']: string
};

type Keys = 'a' | 'b' | 'c';
type Aliased = {
    [key in Keys]: string
};

/**
 * both `Inlined` and `Aliased` have the type
 * `{a: string; b: string; c: string}`
 */

Even though the example in the typescript docs present this feature to be useful in many cases when working with object keys. There are times when there is no need for it.

From the typescript docs:

// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
//   ^ = type KindlessCircle = {
//       radius: number;
//   }

This gets you the same result

type RemoveKindField<T> = {
    [K in Exclude<keyof T, "kind">]: T[K]
};

Where this feature shines is when you would like to remap the keys using template literal type with having the old key to extract the original value type.

Example from typescript docs:

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
//   ^ = type LazyPerson = {
//       getName: () => string;
//       getAge: () => number;
//       getLocation: () => string;
//   }