Learning Typescript in 324-Pages.

Learning Typescript in 324-Pages.

The ABC'S Of Types (Part B)

Today we would be talking about some of the available types provided us by typescript.

The types we would be talking about are: boolean, number, string, objects, array, tuples, enums, any, null, undefined, void.

What is a type?

A type is a way for us to declare what a particular object, variable, class, value etc. has access to in terms of its properties, arguments it takes in, or methods declared on it.

Importance of types?

Types allow us to safely know what can and can't be done with the particular value, object or variable we have at hand.

Primitive Types [number, string, boolean]

Primitive types are essentially the building blocks for more complex types you would be creating along the line.

  1. Number Type: This data type, represents all numbers in typescript. when we say all numbers, it includes NaN, Infinity, Floats etc. We declare a variable to be of type number as follows:

    index.ts
    
    let age = 7 // type inferred by typescript
    let count: number = 9 // explicit type declaration
    
  2. String Type: This data type, represents any string value. we declare a variable to be of type string as follows:

    index.ts
    
    let language = 'JavaScript' // type inferred by typescript
    let name: string = 'TypeScript' // explicit type declaration
    
  3. Boolean Type: This type is either true or false

      index.ts
    
     let isValid = false // type inferred by typescript
     let isActive: boolean = true // explicit type declaration
    

Now haven understood the primitive types, it would be easy for us to use them with other types as we move on in this tutorial.

Array Type

You might have a variable that stores an array of elements or a function that accepts an array as its arguments, so the question comes. How to we type safe this argument or variable.

To type safe an array of something, we say something[]. Now we said something because there are various possibilities of what something could stand for e.g. an array of strings, an array of numbers, an array of boolean, an array of objects etc.

The syntax for typing an array is given below:

index.ts

let evenNumbers: number[] = [2,4,6,...]

Here the say that evenNumbers is of type array but the type of the content in the array is number.

And we could do that for other types like ['apple' 'orange'] would be an array of strings i.e. string[].

Now there are cases were our array could hold different datatypes. How do we go about adding types in such a case?

Lets take an example in which, we want our array to contain a users name, age, and availability. You would most likely not be storing data in this form. An object would be more appropriate but that what we would be using for this example.

So how do we go about that? Unions / Piping

let userData: (string | number | boolean)[] = ["tola", 22, true]

So what we did above is called union or piping / OR operator. What that line of code reads is that, the variable userData could consists of a string OR a number OR a boolean. And that is the way we can combine types together.

Further down this tutorial we would still talk about unions and intersections.

Enum Type

This type allows us to associate known keywords along with the associated numeric value e.g. resource types, menu choices, music genre etc.

This helps us better organize our code in a way and prevent magic values.

See example below


let obj1 = {title: 'Take Five', genre: 1} // where `1` represents Jazz
let obj2 = {title: 'Feeling', genre: 2} // where `2` represents Hip Pop

Now as we can see that code isn't really readable and can be cleaned up a bit.

index.ts

enum Genre {
  Jazz = 1,
  "Hip Pop",
  "Afro Cuban"
}
let obj1 = {title: 'Take Five', genre: Genre.Jazz} //output { title: 'Take Five', genre: 1 }
let obj2 = {title: 'Feeling', genre: Genre["Hip Pop"]} // output { title: 'Feeling', genre: 2 }

Notice how clean our code looks now after making use of enum. we can also extract the keyward given the index i.e. Genre[1] would give use 'Jazz'.

Any Type

Now we would be talking about the any type. Generally I don't like to talk about it but just know you should try to avoid this type as much as possible in your code. Why?

Just as the name connotes, any means it can be anything. Let's take a look at an example to understand the reason we should try to avoid this type as much as possible.

We would be using the classic Promise object in JavaScript. If you have done any asynchronous programming you would have come across this ;

index.js

let callback = (resolve, reject)=>{
    if (someConditionPasses) return resolve(message)
    return reject(new Error(error message))
}
let networkCall = (arg)=> new Promise(arg)
networkCall(callback)

From the code above we see a typical implementation for a promise and we call Instantiate the promise with the right argument.

Now lets move this code to .ts file and type the input to networkCall as any.

index.ts
let networkCall = (arg: any) => new Promise(arg)
networkCall(4) // error is not caught until runtime

Now we don't get any error from typescript because we declared the input argument to a promise as any and this is the problem. It gets rid of the type safety we would have gotten and leds to runtime error ๐Ÿ˜ž.

Hope you see one of the disadvantages of using any now.

Null, Undefined, Void Type

  1. null type: this is used to define something e.g. a variable, that has a value and the value is null.
  2. undefined type: this is used to define something e.g. a variable that has no available value. Like accessing a key that doesn't exist on an object.

    index.ts
    
    let skillLevel = {
    python: 0.7,
    javascript: 0.6
    }
    skillLevel.java  // typescript error Property 'java' does not 
    exist on type '{ python: number; javascript: number; 
    }'.ts(2339)
    

    In Javascript, we would get undefined.

  3. void type: this type is used mainly on functions e.g.

       index.ts
    
       const log: () => void = () => {}; // log: () => void
    

    The above code says that the function log doesn't take in any arguments and it returns a type void.

    void type means the function could return a value but the value should be ignored.

Unions, and Intersections

Union:

A union type is formed from the concatenation of two or more other types, it is a value that can be either of the types combined.

index.ts
type Gender = "male" | "female"

const IsHuman = (gender: Gender) => {...}

Interesection:

Intersections allow us to combine multiple types into a single type that has all the features in each of those combined types.

 index.ts

 interface IStore {
     storeName: string
     storeProducts: object[]
 }

interface IUser {
      firstName: string
      nickName: string
}

type IsUserAndStoreOwner = IUser & IStore

let storeOwner: IsUserAndStoreOwner = {
       firstName: "Samuel",
       nickName: "sammie",
       storeName: "JK Stores",
       storeProducts: []
}

If we fail to implement all attributes of the interfaces, we see the error throwing by typescript below.

let storeOwner: IsUserAndStoreOwner = {
       firstName: "Samuel",
       nickName: "sammie",
       storeName: "JK Stores"
}  // Type '{ firstName: string; nickName: string; storeName: string; }' is not assignable to type 'IsUserAndStoreOwner'.
  Property 'storeProducts' is missing in type '{ firstName: string; nickName: string; storeName: string; }' but required in type 'IStore'.ts(2322)

Type Aliases and Interfaces

Type Aliases:

Type alias is a way to create a name for a defined type. Think of type alias as typical variables you would use in a programming language. Like variables, they are used to store a reference to an actual value. e.g. let a = 7 In this case, a is just an alias to hold the number/value 7.

To create a type alias, see the code below

index.ts

type weekDays = 'monday'|'tuesday'|'wednesday'|'thursday'|'friday'|'saturday'|'sunday';
type userId = string|number;
type validArr = number[];

Interfaces:

Interfaces are used to define the structure of objects or classes.

They can be used to define the structure of a database schema or attributes that should be passed in an object.

To create an interface, see the code below

index.ts

interface IUser {
    firstName: string
    lastName: string
    isDev?: boolean
    getFullName?(): ()=>string
}

After defining either a type alias or interface, you can use them to type a function or variable as you see fit.

See exmples below

index.ts

let getUserById = (id: userId): IUser => {...}

class User implements IUser {...}

Function Call Signatures

This describes how we can type a callable e.g. a function.

See code example below

type addFunction = (a:number, b:number)=>number

const add: addFunction = (a, b) => {...}

As we can see from the code above, we no longer need to add the type explicitly on the actual function declaration.

We would probably talk about function overload in another blog post.

Generics

Now to the part that gets a little bit confusing ๐Ÿ™‚.

Generics allow us to create reusable/flexible code block that can be used on different data types.

We use generics when we don't exactly know the type we would be dealing with and we don't want to restrict our function's behavior to a specific type.

For example, we want to create a filter function ;

index.ts

const filterArr = (arr: number[], callback: (elm: number) => boolean): number[] => {
  let result: number[] = [];

  arr.forEach((element) => {
    if (callback(element)) result.push(element);
  });

  return result;
};

console.log(filterArr([1, 2, 4, 7], (elm) => elm % 2 === 0)); // [2,4]

Now, the above example works perfectly well when passed an array of numbers but what happens we pass in strings or objects ???

index.ts

console.log(filterArr(['1', '2', '4', '7'], (elm) => elm % 2 === 0)); // Type 'string' is not assignable to type 'number'.ts(2322)

We get a typescript error as regards passing in a string when the function expects a number.

Now we want this function to work on any type array. A possible solution is to create this function for the various possible type and type them appropriately but we don't follow the DRY (don't repeat yourself) principle.

Now we can leverage on generics and reimplement this function as follows;

index.ts

const filterArr = <T = number>(arr: T[], cb: (elm: T) => boolean): T[] => {
  let result: T[] = [];

  arr.forEach((element) => {
    if (cb(element)) result.push(element);
  });

  return result;
};

console.log(filterArr([1, 2, 4, 7], (elm) => elm % 2 === 0));
console.log(filterArr(["1", "2", "4", "7"], (elm) => parseInt(elm) % 2 === 0));

Now as we see above, we have made the function to be able to work with any type passed in. In this case our generic type is inferred but can also the stated explicitly below ;

index.ts
console.log(filterArr<number>([1, 2, 4, 7], (elm) => elm % 2 === 0));
console.log(filterArr<string>(["1", "2", "4", "7"], (elm) => parseInt(elm) % 2 === 0));

There is a whole lot more to generics and would probably make more blog posts explaining them and also function overloads.

Part A

Thank you for reading, and let's connect.

Thank you for reading my blog post. You can follow me on Twitter and LinkedIn

ย