Bottom and Top types in typescript
Low-level and top-level types are a mathematical concept of type theory that exists in many programming languages. It also exists in TypeScript and is an important concept that is often overlooked.
TypeSript has two top-level types, any and unknown, and an underlying type, never.
Top level type
When you see a description of a top-level type, you will generally see something like this:
The top-level type is the supertype of all other types.
A type is actually a set of all possible values that can be bound to a variable or parameter.
In basic object-oriented programming, we know that a subtype variable or function parameter can be bound to a supertype.
For example, in the walkies function below, there is an argument named dog, which is of type dog and can therefore be bound to subtypes Pitbull and Alsation.
class Dog {}
class Pitbull extends Dog {}
class Alsation extends Dog {}
function walkies(dog: Dog){}
walkies(new Pitbull());
walkies(new Alsation());
Copy the code
Similarly, the top-level type means that all other types in the type system are subtypes of the top-level type, so we can assign anything to the top-level type variable, for example:
let n: unknown = 5;
let x: any = { foo: 'bar' };
let s: unknown = "foobar";
Copy the code
The following functions topAny and topUnknown can accept arguments of any type.
function topAny(value: any) {}
function topUnknown(value: unknown) {}
topAny({ foo: 'bar' });
topUnknown(8);
topUnknown("8");
Copy the code
The infamous any type
Using any is like letting the program go through the escape door, basically telling the TSC compiler to back off and let the program run without any compile-time type checking.
const func = (dog: any) = > {
dog.bark(); // the compiler trusts you that bark exists on dog
}
Copy the code
The use of any is a return to JavaSript’s weak types and should be avoided, especially now that unknown types have been introduced.
Unknown type
The unknown type is introduced in TypeScript 3, and you can think of it as a type-safe any type, slightly different from any.
Just as variables of all types can be assigned to variables of type any, variables of all types can be assigned to variables of type unknown.
The goal of the unknown type is to be the least likely type. Although anything can be assigned to a variable of unknown type, we need to do type narrowing or conditioning before we can use the variable properly.
Type narrowing
Type narrowing is a very important concept in TypeScript, but it is not well understood.
In the following example, Type Guards are used to narrow the parameter types of the function, as in lines 10 and 14 in the following code, function parameters are narrowed by type to the more specific Dog or Pitbull types.
interface Dog {
bark: () = > void;
}
interface Pitbull extends Dog {
attack: () = > void;
}
const isDog = (value: any): value is Dog => { // Value is Dog is a type guard
returnvalue && !! value.bark; };const isPitbull = (value: any): value is Pitbull => {
return value && value.attack;
};
const walkies = (value: unknown) = > {
if (isPitbull(value)) {
value.bark();
value.attack();
return;
}
if (isDog(value)) {
value.bark();
}
// if we used any instead of unknown then
// the compiler would allow this
// value.bark();
// but with unknown we ge†
// TypeError: Object is of type 'unknown'.
};
Copy the code
There are many limitations to using variables of unknown type directly, but you can use Type Guards to narrow the variable type to a more specific type, and then use variables of that specific type. If you use a variable of type any directly, the compiler does no checking and can call any method.
Having looked at the previous explanation of top-level types, let’s review the definition of top-level types:
The top-level type is the supertype of all other types.
const walkies = (value: unknown) = > {
Copy the code
Just as Dog is Pitbull’s supertype,unknown is a supertype of each type. Therefore, variables of unknown type can accept assignments from variables of any type. However, unlike any type, variables of unknown type cannot call specific methods. Instead, use Type Guards to narrow the type down to a specific type, and then operate.
With the introduction of Unknown, there’s no reason to use any anymore, because using any basically means ignoring the type system.
Underlying type never
Never indicates a type that never occurs. This is known in type categorization as the underlying type.
Examples are as follows:
-
A function that never returns
-
Type Guards returns false
Here is an example of a compiler prompt when an exception is thrown:
// const neverWalkies: (dog: Dog) => never
const neverWalkies = (dog: Dog) => {
throw new Error('No walkies now');
}
Copy the code
This exception means that the function will never return a value, so the return type is never.
The never or underlying type variable has no value. You might think of null or undefined as low-level types, but variables and parameters can be bound to null or undefined as values, and variables of type never represent values that never occur.
So, what does never really mean?
The first use is what we just talked about, for functions that never return.
Another very important use is to trim unwanted values for type conditioning.
Take a look at TypeScript’s built-in Extract utility class definition:
/** * Extract from T those types that are assignable to U */
type Extract<T, U> = T extends U ? T : never;
Copy the code
We can use Extract to create a more specific Tamedog type:
// type TameDog = {
// bark: () => void;
// walk: () => void;
// }
type TameDog = Pick<Dog, Extract<keyof Dog, 'bark' | 'walk' | 'meow'> >;const doggy: TameDog = {
bark() {
console.log('woof woof');
},
walk() {
console.log('good doggy'); }};Copy the code
Extract on line 5 will Extract the common parts of the two arguments to form a new union type, which will then be passed to Pick to construct a new type.
To summarize, a top-level type can be assigned to any type of variable, a low-level type never occurs and has no value, and all other types fall somewhere in between.