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.eACompatible withB.BAlso compatible withA. thenACan be asserted asB.BCan also be asserted asA.
    • Either direction of the assertion in this case can be called secure bidirectional inference.
  • In addition, ifACompatible withB, butBAre not compatibleAIn this caseACan also be asserted asB.BCan also be asserted asA.
    • In this case, willAAssertion isB, called insecure type narrowing.
    • willBAssertion isA, called safe type generalization (personal term).

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:

  • allowanimal as CatBecause “a parent class can be asserted as a subclass,” this is equivalent to less secure type narrowing.
  • allowcat as AnimalSince 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:

    • anyCan be assigned to any type as a value;
    • unknownCan 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:

  • anyCan be assigned to any type as a value;
  • unknownCan 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 toDogInference forCat | DogAnd inference forCatThis is a kind ofhackThe practice of,DogDirectly deduce thatCatMembers 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 asany|unknown
  • any|unknownCan 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