TypeScript type assertions (super verbose)

Type Assertion can be used to manually specify the type of a value.

1. Grammar

value as type

or

<type>value

Note: The value as type must be used in tsx syntax. Because the syntax of the form represents a ReactNode in tsx, in addition to expressing type assertions, it may also represent a generic type in ts.

2. The purpose of type assertion

2.1. Assert a union type as one of the types

It is known that when TypeScript is not sure what type a variable of a union type is, we can only access the properties or methods common to all types of this union type:

interface Cat {<!-- -->
    name: string;
    run(): void;
}
interface Fish {<!-- -->
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {<!-- -->
    return animal.name;
}
interface Cat {<!-- -->
    name: string;
    run(): void;
}
interface Fish {<!-- -->
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {<!-- -->
    if (typeof animal.swim === 'function') {<!-- -->
        return true;
    }
    return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.

Sometimes we really need to access a type-specific property or method when the type is not yet certain. In this case, we can use type assertion to assert animal into Fish:

interface Cat {<!-- -->
    name: string;
    run(): void;
}
interface Fish {<!-- -->
    name: string;
    swim(): void;
}
//Use value as type
function isFish(animal: Cat | Fish) {<!-- -->
    if (typeof (animal as Fish).swim === 'function') {<!-- -->
        return true;
    }
    return false;
}

This will solve the problem of error when accessing animal.swim.

Note: Type assertions can only deceive the TypeScript compiler and cannot avoid runtime errors. Abuse of type assertions may lead to runtime errors

interface Cat {<!-- -->
    name: string;
    run(): void;
}
interface Fish {<!-- -->
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {<!-- -->
    (animal as Fish).swim();
}

const tom: Cat = {<!-- -->
    name: 'Tom',
    run() {<!-- --> console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

The above example will not report an error when compiling, but will report an error when running:

Uncaught TypeError: animal.swim is not a function`

The reason is that the code (animal as Fish).swim() hides the situation where animal may be Cat and replaces animal is directly asserted as Fish, and the TypeScript compiler trusts our assertion, so there is no compilation error when calling swim(). However, the parameter accepted by the swim function is Cat | Fish. Once the parameter passed in is a variable of type Cat, because Cat If there is no swim method on code>, a runtime error will occur. Be extra careful when using type assertions and try to avoid calling methods or referencing deep properties after assertions to reduce unnecessary runtime errors.

2.2. Assert a parent class into a more specific subclass

Type assertions are also common when there is an inheritance relationship between classes:

class ApiError extends Error {<!-- -->
    code: number = 0;
}
class HttpError extends Error {<!-- -->
    statusCode: number = 200;
}

function isApiError(error: Error) {<!-- -->
    if (typeof (error as ApiError).code === 'number') {<!-- -->
        return true;
    }
    return false;
}

In the above example, we declared the function isApiError, which is used to determine whether the incoming parameter is of type ApiError. In order to implement such a function, the type of its parameters must be It has to be a more abstract parent class Error, so that this function can accept Error or its subclasses as parameters.

However, since there is no code attribute in the parent class Error, directly obtaining error.code will result in an error, and you need to use type assertion to obtain (error as ApiError).code

You may notice that in this example there is a more appropriate way to determine whether it is ApiError, that is to use instanceof:

class ApiError extends Error {<!-- -->
    code: number = 0;
}
class HttpError extends Error {<!-- -->
    statusCode: number = 200;
}

function isApiError(error: Error) {<!-- -->
    if (error instanceof ApiError) {<!-- -->
        return true;
    }
    return false;
}

In the above example, it is indeed more appropriate to use instanceof, because ApiError is a JavaScript class that can determine errorinstanceof /code> is an instance of it. But in some cases, ApiError and HttpError are not a real class, but just a TypeScript interface (interface). The interface is a type. It is not a real value. It will be deleted in the compilation result. Of course, instanceof cannot be used to make runtime judgments:

interface ApiError extends Error {<!-- -->
    code: number;
}
interface HttpError extends Error {<!-- -->
    statusCode: number;
}

function isApiError(error: Error) {<!-- -->
    if (error instanceof ApiError) {<!-- -->
        return true;
    }
    return false;
}

// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

At this time, you can only use type assertions to determine whether the passed-in parameter is ApiError by judging whether there is a code attribute:

interface ApiError extends Error {<!-- -->
    code: number;
}
interface HttpError extends Error {<!-- -->
    statusCode: number;
}

function isApiError(error: Error) {<!-- -->
    if (typeof (error as ApiError).code === 'number') {<!-- -->
        return true;
    }
    return false;
}

2.3. Assert any type to any

Ideally TypeScript’s type system works well, and the type of each value is specific and precise. When we reference a property or method that does not exist on this type, an error will be reported:

const foo: number = 1;
foo.length = 1;

// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.

In the above example, the numeric variable foo does not have the length attribute, so TypeScript gives a corresponding error message. This error message is obviously very useful. But sometimes, we are very sure that this code will not go wrong, such as the following example:

window.foo = 1;

// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & amp; typeof globalThis'.

In the above example, we need to add an attribute foo to window, but TypeScript will report an error when compiling, prompting us that window does not exist. code>foo property.

At this point we can use as any to temporarily assert window as any type:

(window as any).foo = 1;

On variables of type any, access to any property is allowed. It should be noted that asserting a variable as any can be said to be the last resort to solve type problems in TypeScript. It is very likely to cover up the real type error, so if you are not sure , don’t use as any.

2.4. Assert any as a specific type

In daily development, we inevitably need to deal with variables of type any. They may be due to the failure of third-party libraries to define their own types, or they may be left over from history or written by others. Bad code may also be caused by the limitations of the TypeScript type system and the inability to accurately define types. When encountering a variable of type any, we can choose to ignore it and let it breed more any. We can also choose to improve it and assert any to a precise type in a timely manner through type assertions to make up for the situation and make our code develop towards the goal of high maintainability. For example, there is getCacheData in the historical code, and its return value is any:

function getCacheData(key: string): any {<!-- -->
    return (window as any).cache[key];
}

Then when we use it, it is best to assert that the return value after calling it is a precise type, which will facilitate subsequent operations:

function getCacheData(key: string): any {<!-- -->
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

In the above example, after we call getCacheData, we immediately assert it as the Cat type. In this way, the type of tom is clarified, and subsequent access to tom will have code completion, which improves the maintainability of the code.

3. Limitations of type assertions

Are there any restrictions on type assertions? Can any type be asserted as any other type? The answer is no –not any one type can be asserted to be any other type. Specifically, if A is compatible with B, then A can be asserted as B, and B can also be asserted as A.

Below we use a simplified example to understand the limitations of type assertions:

interface Animal {<!-- -->
    name: string;
}
interface Cat {<!-- -->
    name: string;
    run(): void;
}

let tom: Cat = {<!-- -->
    name: 'Tom',
    run: () => {<!-- --> console.log('run') }
};
let animal: Animal = tom;

We know that TypeScript is a structural type system, and the comparison between types will only compare their final structures and ignore the relationship between them when they were defined. In the above example, Cat contains all the properties in Animal, and in addition, it has an additional method run. TypeScript doesn’t care about the relationship between Cat and Animal when they are defined, but only looks at the relationship between their final structures – so it is the same as Cat extends Animal is equivalent:

interface Animal {<!-- -->
    name: string;
}
interface Cat extends Animal {<!-- -->
    run(): void;
}

Then it is not difficult to understand why tom of type Cat can be assigned to animal of type Animal – just like In object-oriented programming, we can assign instances of subclasses to variables of type parent class. Let’s change it to a more professional term in TypeScript, that is: Animal is compatible with Cat. When Animal is compatible with Cat, they can type-assert each other:

interface Animal {<!-- -->
    name: string;
}
interface Cat {<!-- -->
    name: string;
    run(): void;
}

function testAnimal(animal: Animal) {<!-- -->
    return (animal as Cat);
}
function testCat(cat: Cat) {<!-- -->
    return (cat as Animal);
}

This design is actually easy to understand:

  • animal as Cat is allowed because “parent classes can be asserted as subclasses”
  • cat as Animal is allowed because since the subclass has the attributes and methods of the parent class, it will not be asserted as the parent class to obtain the attributes of the parent class and call the methods of the parent class. Any problem, so “A subclass can be asserted as a parent class”

In summary:

  • A union type can be asserted as one of the types
  • Parent classes can be asserted as subclasses
  • Any type can be asserted as any
  • any can be asserted to be of any type
  • For A to be asserted as B, A only needs to be compatible with B or B > Compatible with A

In fact, the first four situations are all special cases of the last one.

4. Double assertion (disabled)

Since any type can be asserted as any, and any can be asserted as any type, can we use double assertion as any as Foo to assert any type to any other type?

interface Cat {<!-- -->
    run(): void;
}
interface Fish {<!-- -->
    swim(): void;
}

function testCat(cat: Cat) {<!-- -->
    return (cat as any as Fish);
}

In the above example, if you use cat as Fish directly, you will definitely get an error because Cat and Fish are incompatible with each other. But if you use double assertion, you can break the “For A to be asserted as B, only A needs to be compatible with B code> or B is compatible with A“, asserting any type as any other type. If you use this kind of double assertion, it is very wrong in all likelihood, and it is likely to cause a runtime error.

5. Type assertion vs type conversion

Type assertions will only affect types when TypeScript is compiled, and type assertion statements will be deleted in the compilation results

function toBoolean(something: any): boolean {<!-- -->
    return something as boolean;
}

toBoolean(1);
//The return value is 1

In the above example, asserting something as boolean can be compiled, but it is of no use. After compilation, the code will become:

function toBoolean(something) {<!-- -->
    return something;
}

toBoolean(1);
//The return value is 1

So type assertion is not type conversion, it does not really affect the type of the variable. To perform type conversion, you need to directly call the type conversion method (such as Boolean())

function toBoolean(something: any): boolean {<!-- -->
    return Boolean(something);
}

toBoolean(1);
//The return value is true

6. Type assertion vs type declaration

In this example, we assert the any type to the Cat type using as Cat.

function getCacheData(key: string): any {<!-- -->
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

But there are actually other ways to solve this problem:

function getCacheData(key: string): any {<!-- -->
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom: Cat = getCacheData('tom');
tom.run();

In the above example, we declare tom as Cat through type declaration, and then use any type getCacheData( 'tom') is assigned to tom of type Cat. This is very similar to type assertion, and the result is almost the same – tom becomes of type Cat in the following code. Their differences can be understood through this example:

interface Animal {<!-- -->
    name: string;
}
interface Cat {<!-- -->
    name: string;
    run(): void;
}

const animal: Animal = {<!-- -->
    name: 'tom'
};
let tom = animal as Cat;

**In the above example, since Animal is compatible with Cat, animal can be asserted as Cat and assigned to tom. **However, if tom is directly declared as Cat type, an error will be reported, and animal is not allowed to be assigned to Cat Type of tom.

interface Animal {<!-- -->
    name: string;
}
interface Cat {<!-- -->
    name: string;
    run(): void;
}

const animal: Animal = {<!-- -->
    name: 'tom'
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

Animal can be regarded as the parent class of Cat. Of course, instances of the parent class cannot be assigned to variables of type subclass. In depth, their core differences are:

  • animal is asserted as Cat, only if Animal is compatible with Cat or Cat is compatibleAnimal is enough
  • animal is assigned to tom only if Cat is compatible with Animal, but Cat Not compatible with Animal.

In the previous example, since getCacheData('tom') is of type any, any is compatible with Cat, Cat is also compatible with any, so

const tom = getCacheData('tom') as Cat;

Equivalent to

const tom: Cat = getCacheData('tom');

Knowing their core differences, you will know that type declarations are more strict than type assertions, so in order to increase the quality of the code, we’d better use type declarations first.

7. Type assertion vs. generics

function getCacheData(key: string): any {<!-- -->
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

We have a third way to solve this problem, and that is generics:

function getCacheData<T>(key: string): T {<!-- -->
    return (window as any).cache[key];
}

interface Cat {<!-- -->
    name: string;
    run(): void;
}

const tom = getCacheData<Cat>('tom');
tom.run();

By adding a generic to the getCacheData function, we can implement constraints on the return value of getCacheData in a more standardized way. This also removes any from the code, which is the best solution.