[TypeScript] 007 type assertion

Posted by biopv on Sat, 25 Dec 2021 15:56:16 +0100

8. Type assertion

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

grammar

value as type

or

<type>value

The former must be used in tsx syntax (ts version of jsx syntax of React), that is, the value as type.

Syntax such as < foo > represents a ReactNode in tsx. In ts, in addition to representing type assertions, it may also represent a ReactNode generic paradigm.

Therefore, it is recommended that you use the syntax of value as type uniformly when using type assertion, and this idea will be implemented in this book.

Use of type assertions

Assert a union type as one of the types

As mentioned earlier, when TypeScript is not sure which type the 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;
}

Sometimes, we really need to access one of the type specific properties or methods when we are not sure about the type, such as:

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

In the above example, get animal Swim will report an error.

At this time, you can use type assertion to assert animal as Fish:

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

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

This solves the problem of accessing animal The swim times is wrong.

It should be noted that type assertion can only * * cheat * * TypeScript compiler, which can not avoid runtime errors. On the contrary, abuse of type assertion 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 (animal as Fish) The code of swim() hides the possibility that animal may be Cat. It directly asserts animal 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 Cat type variable, there will be a runtime error because there is no swim method on Cat.

In conclusion, you must be very careful when using type assertions, try to avoid calling methods or deep attributes after assertions, so as to reduce unnecessary runtime errors.

Assert a parent class as 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 judge whether the incoming parameter is of ApiError type. In order to implement such a function, its parameter type must be the more abstract parent class Error. In this way, the function can accept Error or its subclasses as parameters.

However, since there is no code attribute in the parent class Error, you can directly obtain Error Code will report an Error, which needs to be obtained using type assertion (Error as apierror) code.

You may notice that in this example, there is a more appropriate way to judge whether it is ApiError, that is, 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, instanceof is indeed more appropriate, because ApiError is a JavaScript class, which can judge whether error is its instance through instanceof.

However, in some cases, ApiError and httprror are not a real class, but just a TypeScript interface. The interface is a type, not a real value. It will be deleted in the compilation result. Of course, instanceof cannot be used for runtime judgment:

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 assertion to judge whether the passed in parameter is ApiError by judging whether the code attribute exists:

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

Assert any type as any

Ideally, TypeScript's type system works well, and the type of each value is specific and precise.

When we refer to 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, there is no length attribute on the variable foo of numeric type, so TypeScript gives the corresponding error prompt.

This kind of error prompt is obviously very useful.

But sometimes, we are very sure that this code will not make mistakes, such as the following example:

window.foo = 1;

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

In the above example, we need to add a foo attribute to the window, but an error will be reported when TypeScript is compiled, indicating that there is no foo attribute on the window.

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

(window as any).foo = 1;

Access to any property is allowed on a variable of type any.

It should be noted that asserting a variable as any is the last means to solve the type problem in TypeScript.

It's very likely to cover up real type errors, so don't use as any if you're not very sure.

In the above example, we can also solve this error by * * extending the type of window (TODO) * *. However, it will be more convenient to add foo attribute as any temporarily.

In short, on the one hand, we should not abuse as any, on the other hand, we should not completely deny its role. We need to strike a balance between the strictness of types and the convenience of development (this is also true) Design concept of TypeScript One) to maximize the value of TypeScript.

Assert any as a concrete type

In daily development, we inevitably need to deal with variables of any type. They may be because the third-party library fails to define its own type, they may be bad code left over from history or written by others, or they may be scenarios in which the type cannot be accurately defined due to the restrictions of TypeScript type system.

When we encounter a variable of type any, we can choose to ignore it and let it breed more any.

We can also choose to improve it. Through type assertion, we can timely assert any as an accurate type, and make up for the lost sheep, so as to make our code develop towards the goal of high maintainability.

For example, there is a getCacheData in the code left over by history, and its return value is any:

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

When we use it, we'd better assert the return value after calling it into an exact type, which facilitates 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, we assert getCacheData as Cat type immediately after calling it. In this way, the type of tom is defined, and the subsequent access to tom will have code completion, which improves the maintainability of the code.

Restrictions on type assertions

Pre knowledge points of this section: structure type system (TODO), type compatibility (TODO)

From the above example, we can conclude:

  • A union type can be asserted as one of these types
  • A parent class can be asserted as a child class
  • Any type can be asserted as any
  • Any can be asserted as any type

So what are the limitations of type assertions? Can any type be asserted as any other type?

The answer is no -- not every type can be asserted as any other type.

Specifically, if A is compatible with B, A can be asserted as B, and B can also be asserted as A.

Let's understand the limitations of type assertion through a simplified example:

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

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

As we know, TypeScript is a structural type system. The comparison between types will only compare their final structure, and ignore the relationship when they are defined.

In the above example, Cat contains all the attributes in Animal. In addition, it has an additional method run. TypeScript does not care about the relationship between Cat and Animal when they are defined, but only depends on the relationship between their final structure - so it is equivalent to Cat extensions Animal:

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

It is not difficult to understand why tom of Cat type can be assigned to animal of animal type - just as in object-oriented programming, we can assign instances of subclasses to variables of parent type.

We changed it to a more professional term in TypeScript, that is, Animal is compatible with Cat.

When Animal is Cat compatible, 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);
}

In fact, such a design can be easily understood:

  • animal as Cat is allowed because "a parent class can be asserted as a child class", which has been learned earlier
  • cat as Animal is allowed because since the subclass has the properties and methods of the parent class, there will be no problem in being asserted as the parent class, obtaining the properties of the parent class and calling the methods of the parent class. Therefore, "the subclass can be asserted as the parent class"

It should be noted that here we use the simplified relationship between parent and child classes to express type compatibility. In fact, TypeScript is much more complicated than this when judging type compatibility. Please refer to the chapter [type compatibility (TODO)] [] for details.

In short, if A is compatible with B, then A can be asserted as B and B can also be asserted as A.

Similarly, if B is compatible with A, A can be asserted as B, and B can also be asserted as A.

So this can be put another way:

To enable a to be asserted as B, only a is compatible with B or B is compatible with A. This is also for the safety consideration of type assertion. After all, unfounded assertion is very dangerous.

in summary:

  • A union type can be asserted as one of these types
  • A parent class can be asserted as a child class
  • Any type can be asserted as any
  • Any can be asserted as any type
  • To enable A to be asserted as B, only A is compatible with B or B is compatible with A

In fact, the first four cases are the last special case.

Double assertion

since:

  • Any type can be asserted as any
  • Any can be asserted as any type

So can we use double assertion as any as Foo to assert any type as any other type?

I think I'm you, and you think you're me!

interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);
}

In the above example, if cat as Fish is used directly, an error will be reported because Cat and Fish are incompatible with each other.

However, if double assertion is used, the restriction that "to enable A to be asserted as B, only A is compatible with B or B is compatible with A" can be broken, and any type can be asserted as any other type.

If you use this double assertion, nine times out of ten it is very wrong and it is likely to lead to runtime errors.

Never use double assertions unless you have to.

Type assertion vs type conversion

Type assertion only affects the type of TypeScript at compile time, and the type assertion statement will be deleted in the compile result:

It's like opening the back door. It was "wrong" and let me pass!

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 useless. After compilation, the code will become:

function toBoolean(something) {
    return something;
}

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

So type assertion is not a type conversion, it does not really affect the type of variable.

To perform type conversion, you need to directly call the method of type conversion:

function toBoolean(something: any): boolean {
    return Boolean(something);
}

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

Type assertion vs type declaration

In this example:

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 use as Cat to assert any type as Cat type.

But there are 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 assign getCacheData('tom ') of any type to tom of Cat type.

This is very similar to type assertion, and the result is almost the same -- tom becomes Cat type in the next 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 Cat compatible, you can assign an animal assertion as Cat to tom.

However, if tom is declared as Cat type directly:

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

An error will be reported, and it is not allowed to assign animal to tom of Cat type.

It is easy to understand that Animal can be regarded as the parent class of Cat. Of course, the instance of the parent class cannot be assigned to a variable of type subclass.

In depth, their core differences lie in:

  • If Animal is asserted as Cat, it only needs to meet the requirements of Animal compatible Cat or Cat compatible Animal
  • The assignment of Animal to tom needs to meet the requirements of Cat compatible Animal

However, Cat is not compatible with Animal.

In the previous example, because getCacheData('tom ') is of any type, any is compatible with Cat, and 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 know that type declarations are more stringent than type assertions.

Therefore, in order to increase the quality of code, we'd better give priority to type declaration, which is more elegant than the as syntax of type assertion.

Type assertion vs generics

Pre knowledge points of this section: generic paradigm

Another example:

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 also have a third way to solve this problem, 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 < T > to the getCacheData function, we can more standardize the constraints on the return value of getCacheData, which also removes any in the code. It is an optimal solution.

Topics: TypeScript