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 error
/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 asB
,A
only needs to be compatible withB
orB
> Compatible withA
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 asCat
, only ifAnimal
is compatible withCat
orCat
is compatibleAnimal
is enoughanimal
is assigned totom
only ifCat
is compatible withAnimal
, butCat
Not compatible withAnimal
.
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.