Table of contents
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.4 Asserting any as a concrete type
Limitations of Type Assertions
Type assertion vs type conversion
Type assertion vs type declaration
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 B
, A
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')
Cat
tom
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.