Type assertions in typeScript

Table of contents

1. Type assertion

grammar

 2. The purpose of type assertion

2.1 Asserting a union type as one of the types 

 2.2 Asserting a parent class as a more specific subclass

 2.3 Assert any type as any

2.4 Asserting any as a concrete type

Limitations of Type Assertions

double affirmation

Type assertion vs type conversion

Type assertion vs type declaration

Type Assertions vs Generics


1. Type assertion

Type assertions (Type Assertion) can be used to manually specify the type of a value. Generally used in joint types.

grammar

value as type

or

<type> value

The former must be used in tsx syntax (the ts version of React's jsx syntax), ie  值 as 类型.

In tsx, the syntax like this  <Foo> represents one  ReactNode, and in ts, besides representing a type assertion, it may also represent a generic type .

值 as 类型 Therefore, it is recommended that you use this syntax uniformly when using type assertions  , and this idea will also be implemented in this book.

 2. The purpose of type assertion

2.1 Asserting a union type as one of the types 

When TypeScript is not sure which type a variable of a joint type is, we can only access properties or methods common to all types of this joint type :

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

function getName(animal: Cat | Fish) {
    return animal.name;
}

 And 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,  animal.swim an error will be reported when obtaining.

At this point, you can use type assertion, which will be  animal asserted 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 will solve  animal.swim the problem of error reporting when accessing.

It should be noted that type assertions can only "cheat" the TypeScript compiler, and cannot avoid runtime errors. On the contrary, abusing 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  (animal as Fish).swim() this code hides  animal the possible  Cat situation, and will  animal directly assert Fish that  , and the TypeScript compiler trusts our assertion, so swim() there is no compilation error when calling.

But  swim the parameter accepted by the function is  Cat | Fish, once the parameter passed in is  Cat a variable of type, because  Cat there is no method on it  swim , it will cause a runtime error.

In short, you must 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 Asserting 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 declare a function  isApiError, which is used to determine whether the incoming parameter is  ApiError a type. In order to implement such a function, the type of its parameter must be a relatively abstract parent class  Error, so that this function can accept  Error or its subclass as a parameter.

Error However, since there is no attribute in  the parent class  ,  an error will be reported  code if it is obtained directly  , and a type assertion is required to obtain it .error.code(error as ApiError).code

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

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 incoming  code parameter is correct by judging whether there is an attribute  ApiError :

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 Asserting any type as any

Ideally, TypeScript's type system works well enough that each value's type 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,  foo there is no  length attribute on the variable of the number type, 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 & typeof globalThis'.

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

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

(window as any).foo = 1;

 On  any variables of type, access to any property is allowed.

It's important to note that asserting a variable  any is arguably a last resort solution to type problems in TypeScript.

It's highly likely to mask a real type error, so don't use it if you're not absolutely sure  as any.

 In the above example, we can also solve this error by [extending the type of window (TODO)][], but it will be more convenient if it is only a temporary addition of  foo attributes .as any

In short, on the one hand, it can’t be abused  as any, and on the other hand, don’t completely negate its role. We need to strike a balance between the strictness of the type and the convenience of development (this is also  one of TypeScript’s design concepts ), in order to maximize the value of TypeScript.

2.4  any Asserting to a concrete type

In daily development, we inevitably need to deal with  any type variables. 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 bad code written by others, or they may be limited by the TypeScript type system and cannot precisely define types.

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

any We can also choose to improve it, and through type assertion , we can assert the exact type in time  , make up for it, and make our code develop towards the goal of high maintainability.

For example, there is one in the legacy code  getCacheData, 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 the return value after calling it into an accurate 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 example above, we  getCacheData asserted it as  Cat a type immediately after calling it. In this way, if the type is clarified  tom ,  tom there will be code completion for subsequent accesses, which improves the maintainability of the code.

Limitations of Type Assertions

From the above example, we can conclude that:

  • A union type can be asserted as one of the 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 are there any restrictions on type assertions? Can any type be asserted as any other type?

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

Specifically, if  A compatible  BA can be asserted as  B, B and can be asserted as  A.

Let's understand the limitations of type assertions 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;

 We know that TypeScript is a structural type system, and the comparison between types will only compare their final structures, while ignoring the relationship when they were defined.

In the example above, all the properties in Cat are included  Animal , in addition to that, it has an additional method  run. TypeScript doesn't care  what the relationship between Cat and  Animal is when they're defined, but only how their final structure is - so it's  Cat extends Animal equivalent to:

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

Then it is not difficult to understand why  Cat the type  tom can be assigned to  Animal the type  animal -just like in object-oriented programming, we can assign an instance of a subclass to a variable whose type is the parent class.

We change it to a more professional term in TypeScript, namely: Animal Compatible  Cat.

double affirmation

now that:

  • 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?

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 it directly,  cat as Fish you will definitely report an error, because  Cat and  Fish are not compatible with each other.

But if you use double assertion, you can break the restriction of "to  A be asserted  B, you only need to  A be compatible  B or  B compatible  A ", and assert any type as any other type.

If you use this kind of double assertion, then nine times out of ten it is very wrong, and it will most likely cause a runtime error.

Never use double assertions unless absolutely necessary.

Type assertion vs type conversion

Type assertions only affect the type when TypeScript is compiled, and the type assertion statement will be deleted in the compilation result:

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

toBoolean(1);
// 返回值为 1

In the above example, it will  something be asserted that  boolean although it can be compiled, it is not useful. After compilation, the code will become:

function toBoolean(something) {
    return something;
}

toBoolean(1);
// 返回值为 1

So a type assertion is not a type conversion, it doesn't really affect the type of the variable.

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

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

toBoolean(1);
// 返回值为 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 type  any assertion for  Cat types.

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  tom declare as  by way of type declaration Cat, and then   assign  any type's  to  type's  .getCacheData('tom')Cattom

This is very similar to a type assertion, and produces almost the same result - it tom becomes  Cat a type in the rest of the code.

Type Assertions vs Generics

Still 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 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  getCacheData adding a generic type to the function  <T>, we can achieve more standardized  getCacheData constraints on the return value, which also removes the code in the code  any, which is an optimal solution.

Guess you like

Origin blog.csdn.net/qq_57423665/article/details/130613173