In normal typescript development, sometimes you understand a value’s type better than the TS compiler does, usually by narrowing what the compiler has inferred about the type. This is where you use type assertions. Here’s the syntax:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
Copy the code
The other is the as syntax:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
Copy the code
Restrictions on type assertions
Since type assertions are so powerful that they can override what the compiler already knows about a value type, there must be some limitation to this dangerous technique.
Not all types can be type asserted, provided that there is a parent-child/hyperon type relationship between the two types. In detail, there are two cases:
- If two types are compatible with each other, i.e
A
Compatible withB
.B
Also compatible withA
. thenA
Can be asserted asB
.B
Can also be asserted asA
.- Either direction of the assertion in this case can be called secure bidirectional inference.
- In addition, if
A
Compatible withB
, butB
Are not compatibleA
In this caseA
Can also be asserted asB
.B
Can also be asserted asA
.- In this case, will
A
Assertion isB
, called insecure type narrowing. - will
B
Assertion isA
, called safe type generalization (personal term).
- In this case, will
Let’s use a simplified example to understand the limitations of type assertions:
interface Animal {
type: string;
}
interface Cat {
type: string;
miao(): void;
}
let tom: Cat = {
type: 'land_animal'
miao: () = > console.log('miao');
};
let animal: Animal = cat;
Copy the code
As we know, TypeScript is a structure-type system, and comparisons between types only compare their final structure, ignoring the relationships they were defined in. Cat extends Animal.
Animal is compatible with Cat. When Animal is compatible with Cat, they can make type assertions to each other:
interface Animal {
type: string;
}
interface Cat {
type: string;
miao(): void;
}
function cookAnimal(animal: Animal) {
return (animal as Cat);
}
function cookCat(cat: Cat) {
return (cat as Animal);
}
Copy the code
This design is actually easy to understand:
- allow
animal as Cat
Because “a parent class can be asserted as a subclass,” this is equivalent to less secure type narrowing. - allow
cat as Animal
Since a subclass owns the attributes and methods of its parent class, there is no problem with being asserted as a parent class, obtaining the attributes of the parent class, and calling the methods of the parent class. Therefore, “a subclass can be asserted as a parent class” is equivalent to a safe type generalization.
Note that TypeScript is much more complex in determining type compatibility than we use a simplified parent/child relationship to express type compatibility.
Value \ variable | any | unknown | object | void | undefined | null | never |
---|---|---|---|---|---|---|---|
Any – | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
The unknown – | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
The object – | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
The void – | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
Undefined – | ✓ | ✓ | – | ✓ | – | ✕ | |
The null – | ✓ | ✓ | – | – | – | ✕ | |
He never – | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
The columns represent the type of the variable (constant) and the rows represent the value to be assigned to the variable (constant). – indicates a type that is compatible only if –strictNullChecks is closed.
-
A value of any type can be assigned to a variable of its corresponding type.
-
Any and unknown can be thought of as superclasses of all types, accepting values of any type when used as variables. But when it comes to merit, it’s a little different:
any
Can be assigned to any type as a value;unknown
Can only be assigned toany
;
-
Never is a subtype of all types, and a value of never can be assigned to variables of all types. But the never variable can only accept never.
-
The void variable accepts any, undefined, and never. But as a value can only be assigned to any and unknown.
-
Undefined can accept any and never as variables, and can be assigned to any, unknown, and void as values.
-
Null behaves much the same as undefined, except that void is not assigned.
Let’s use a few practical examples.
Undefined and void
In type compatibility, the relationship between the two is officially stated as follows:
Undefined can accept any and never as variables, and can be assigned to any, unknown, and void as values.
Void void void void void void void void void void void void void void void void void void void void
const vod = () = > {};
vod() as undefined; // Type narrowing
let undef: undefined;
undef as void; // Type generalization
Copy the code
Any, unknown, and any type
Type compatibility is explained as follows:
Any and unknown can be thought of as superclasses of all types, accepting values of any type when used as variables. But when it comes to merit, it’s a little different:
any
Can be assigned to any type as a value;unknown
Can only be assigned toany
;
Any is compatible with all types. Unknown is a superclass of all types. Two types of unknown are predictors of all types.
// As unknown as any type
// As any as any
let a = 1;
a as any as string
a as unknown as string
Copy the code
This is certainly wrong and exploits a vulnerability in type assertions, but it can be useful in some special cases.
The joint type
The union type is the parent type of the union member, so any union type can be inferred to be its member, and its member can also be inverse to the union type.
Here’s an interesting example:
In this case, we’re going toDog
Inference forCat | Dog
And inference forCat
This is a kind ofhack
The practice of,Dog
Directly deduce thatCat
Members of a union type cannot push each other.
Function types
First of all, we need to know that function types must be compatible, function parameters must be contravariant, and the return value must be covariant.
Let’s look at the following example, again Cat and Dog from the above example, first of all, CDToCD is assigned to CToCD to satisfy the parameter backplay, that’s ok. CToCD assigned to CDToCD does not, so it is incompatible to write:
How to get the second line of code to pass without reporting an error is very simple, two assertions:
type CDToCD = (c: Cat | Dog) = > Cat | Dog;
type CToCD = (c: Cat) = > Cat | Dog;
const fn1: CDToCD = ((cd: Cat) = > ({ miao(){}}))as CToCD as CDToCD;
Copy the code
Double assertion
We used several double assertions in the previous example. In practice, you can use this technique to do type inference that the compiler considers incompatible, but you think is compatible, since:
- Any type can be asserted as
any|unknown
any|unknown
Can be asserted to any type
Then we can use the double assertion as any as Foo to assert any type as any other type. For example, in react-use, because the type returned by a function wrapped in useCallback cannot be correctly inferred, this technique is used:
constuseAsyncFn = <T extends FuncReturnPromise>( fn: T, deps: DependencyList = [] ): AsyncFnReturn<T> => { const [asyncState, setAsyncState] = useState< StateFromFunctionReturningPromise<T> >({ loading: false, }); const isMount = useMountedState(); const fetch = useCallback((... args: Parameters<T>): ReturnType<T> => { ! asyncState.loading && setAsyncState({ loading: true, }); return fn(... args) .then((res) => { isMount() && setAsyncState({ loading: false, value: res }); }) .catch((err) => { isMount() && setAsyncState({ loading: false, error: err }); }) as ReturnType<T>; }, deps); return [asyncState, fetch as unknown as T]; };Copy the code