Types

TypeScript uses the so-called duck-typing method to provide type safety. This method compares one object with other objects by checking whether both objects have the same type of matching names or not. This checking mechanism is used in computer programming to verify whether an object may be utilized for a specific purpose. TypeScript type checking is used like "stencil". The term duck-typing comes from the English expression duck test:

If something looks like a duck, swims like a duck, and quacks like a duck, it's most likely a duck.

Types

I. Primitive types

  1. Boolean

let isTrue: boolean = true

  1. Number - numbers of any numeral system except BigInt. Every possible number is of a number type, there is no equivalent to int or float

let num: number = 10; 
let float: number = 10.5; 
let binary: number = 0b101;

  1. BigInt - BigInt is a "special number", we cannot mix BIgInt with Number type by assignment or other operations.

BigInt type of value has to be used for the value that is greater than this constant variable:

Number.MAX_SAFE_INTEGER

A value that is greater than Number.MAX_SAFE_INTEGER is not a Number anymore, it is a BigInt.

let bigIntVar: bigint = 100n;
num = bigint; // ERROR

Line 2 throws a compile error, Number cannot be assigned to BigInt, or vice versa.

  1. String

let str:string = "string";

  1. Null

Some refer to Null as the Object, others to a primitive. It is more convenient to consider Null as a primitive, but there is also a strong argument that Null is an Object, namely:

typeof null === object is true.

Some say it's just a bug in the language and some say this is done to explicitly indicate that Null is exactly an empty Object, i.e. that there is no object there.

let nullVar: null = null;

  1. Undefined

Undefined is used when the property is not explicitly defined, so the value is not assigned, Null is used when the value has been assigned, so we need to specify either some meaningful value or null

let undefinedVar: undefined = undefined;

The result of the following checking statement will be "false" if the strict mode in the compiler options is set to true (turned on), but Null will be equal to Undefined if strict mode is turned off.

nullVar === undefinedVar

II. Complex types

  1. Object

For the Object we have to declare the structure of the object after the colon, what structure of the object will it be.

let objectVar: { 
    a: string, 
    b: number, 
    c: { d: boolean } 
} = { a: "", b: 1, c: { d: true } }; 
objectVar = { a: "", b: 1, c: { d: false } }; // it will work just fine

  1. Array

let arrayVar: number[][] = [[1], [2], [45], [1, 2, 4, 5, 99]];

Array<number[]> and Array<Array>- similar ways to define an Array, but via Generics (Please see the corresponding chapter to understand Generics);

arrayVar = []; is aso a valid input. There is nothing to check if there are no elements, and TS still considers this to be a valid Array input;

  1. Tuple

An array of mixed types can be created with a Tuple - it's an array of a specific length, and the TS definition of a tuple captures the types of each element. There are no Tuples in JS, only in TS.

let tuple: [number, string, boolean, {a: bigint} ] = [1, "2", true, {a: 10n}];

Problem with Tuple type: you can use push() and pop() methods with Tuple, and TS will not complain about it, i.e. you can break the defined type by doing tuple.push("s"). If you would use this method, a new element will be added to the tuple, and it will break the type's notation that we defined above.

Tuples are often used in React:

const [state, setState] = useState();

  1. Any

Basically, by using any, we "turn off" TS checking.

let anyVar = {}; 
anyVar = "str"; 
anyVar = 2; 
anyVar.toUpperCase(); // ERROR!

There will be an error in the code above, we are trying to use toUpperCase() a method for a value of Number type, but TS will not help us to catch it because any is kind of a way of saying "TS, please turn off your checking algorithms for this variable". We should avoid any type by all means.

  1. Unknown

This type has come to our help to "replace" any - it's much much safer than any.

We can use unknown when we don't know what type we are going to work with. For example, the given data came from the backend - this is unknown. If the incoming data from the backend is well-known and the backend does not break any contracts, then it is much better to define a more specific type. But if the data comes from an incomprehensible API, then the unknown type is a good choice. When we use any - we can take any property and define it as any, then we can easily get an error as in the code above, but unknown will be able to determine the type of the passed value and help validate it automatically.

let unknownVar: unknown = {a: ""};
unknownVar.upperCase(); // cannot be used
unknownVar.a.upperCase(); // cannot be used

Whatever we are trying to do with the value of unknown type, TS will start complaining. In the code above, we are trying to access the fields and methods of unknown value, and TS will not allow us to do that. This makes it safe to work with unknown values. We cannot call upperCase() method or any other methods for the object we specified. And even the fields of the unknown object cannot be accessed, i.e. the value is "closed" from all sides for use and access.

num = unknownVar // cannot be assigned

TS will not allow you to make this assignment, unknown cannot be assigned to anything, while you can assign anything for the element of any type.

It turns out that nothing can be done with the value of unknown type. But there is a mechanism to move from unknown to a specific type by using Type Guards.

  • What to do if we see a value of unknown type:

  1. Check for a specific type to identify the real type (for example, by using operator typeof);

  2. If it is an object, then all properties must also be separately checked and their real type has to be revealed;

  3. After we checked everything using Type Guards and identified the real type, we explicitly cast this type and work with it as with a normally defined type;

  4. If the type check fails, then we throw an error or a warning;

The order of work if we see unknown: We put a type check; If it is an object, then all properties must also be checked; And after we checked everything using TypeGuard and identified the type, we explicitly cast this type and work with it as with a regular defined type; If the type check fails, then we throw an error or a warning;

If it is hard to understand do not worry, we will get to the Type Guards chapter soon enough. You can find more information about Type Guards in the corresponding chapter.

  1. Never

This type usually is used in scenarios that should never happen, or for code that should never be executed (for example, infinite loops or infinite recursion). It is actively used in Service Types, in scenarios when we want to show that something is not working correctly. Also, it is used in service Conditional Types to explicitly indicate an impossible scenario.

13. Function

  • Function Declaration and Function Expression are typed in the same way.

  • Arguments and return type are both typed.

function func(a: string): boolean {
    return true;
}

const funcArrow = (a:string): boolean => { return true };
const funcArrow = (a:string): undefined => { return };
const funcArrow = (a:string): null => { return null };
const funcArrow = (a:string): void => { }; // no return
const funcArrow = (a:string): void => { return };

Code in line #9 is fine for TS, but nevertheless, it is still better in this scenario to set explicitly the return type undefined, so that it is clear what we are working with.

The following code shows the typing of a callback as an argument. It is not a function implementation, but a specification of the type we define, a template of the callback that this function is able to take as an argument.

const hoc = (callback: (b:string) => boolean): void => { };

14. Literal Types

To use Literal Types we set specific values that we can put in a variable, limiting the variable not just by a specific type, but by specific values.

n of the type we define, a template of the callback that this function is able to take as an argument.

let fontWeight: 500 | 600 | 700 | "bold" = 500;

Operations with Types: Операции с типами:

  1. Intersection Type

let intersectionVar: { a: boolean } & { b: string }; 

The result of this intersection type will be the following expression:

{ a: boolean, b: string  }

This is how a strict type would look like in this case:

let intersectionVar: { a: boolean, b: string };

because this is still exactly the intersection of 2 types, which means that many different scenarios will fit our defined Intersection Type. All we ask in our type is that both elements (a:boolean & b:string) must be present in the object, but there can be many more elements and this will not be a problem. We just care about having those 2 elements together all the time, but many more elements can coexist with them.

t type would look like in this case:

let intesectionVar: number & string; // never

Intersection of 2 Number & String is type Never, because sets of strings and numbers cannot have intersections, one element cannot be a Number & a String at the same time.

  1. Union Type

In the following example, the return type is a Union Type that receives a set of Number and a set of Null.

function sum(numbers: (number | string)[]): number | null {
    if (numbers.length) {
        return numbers.reduce<number>((acc, num) => acc + Number(num) || 0, 0)
    }

    return null;
}

When we define Intersection Type, we mean that both elements will be defined, we expect A & B to be defined together and together only. When we define Union Type - we expect only one resulting element - either one or the other.

Last updated