Learn Typescript from Scratch

Dec 13, 2022
49 min read
9624 words

In this article, I'll give a brief introduction to Typescript. I'll cover most of the topics with examples as well. After reading this post, you can use typescript in your projects (using typescript in React or other frameworks is out of reach). Let's get down to business.

What is Typescript?

TypeScript adds additional syntax to JavaScript to support a tighter integration with your editor. Catch errors early in your editor. TypeScript code converts to JavaScript, which runs anywhere JavaScript runs: In a browser, on Node.js or Demo, and in your apps. TypeScript understands JavaScript and uses type inference to give you great tooling without additional code.

  • Typescript is a superset of JavaScript
  • It doesn't add more features.
  • It allows you to code in a manner so that your code faces much less error in the run time or production.

ts

Don't use typescript if your project is small. You need to use the superpower of the typescript if you are using it to make your code bug and error-free. It's all about the Type safety

> 2 + "2"
> '22' // ans
 
> null + 2
> 2
 
> undefined + 2
> NaN

This should not be done as it makes the issue bigger.

What does typescript do?

It does static checking - whenever you are writing the code, then the code is constantly monitored by IDE to check if you are making any syntax error or something but not in JavaScript. Whatever you write, it's ok

But when you run the code in your environment, then it throws the error. It would be very helpful to get the idea of whether what you are doing is correct or not as you write the code.

Analyze the code as we type. That's it

How does Development Process work?

You write all your code in typescript format and that code is converted to JavaScript. typescript is a development tool and your project still run JavaScript as the browser doesn't understand Typescript.

That's why when you install the typescript package then you download it as a dev dependency.

typescript Playground: Here you can play with typescript and check how it is converted.

index.ts

let car = {
  module: "xyz",
  color: "red",
};
 
// โŒ ERROR: Property 'price' does not exist on type '{ module: string; color: string; }'.
car.price;

As the above code shows in the example we are trying to get the price which does not exist in the object and it shows the error before running the code.

index.ts

let num1 =  2;
let num2 =  "2"
 
// ๐Ÿ‘‡ It works but shouldn't be done right?
let sum = num1 + num2; // "22"

The above code does not show any kind of error and when you run it will show you the result as 22 which we don't want. It is allowed, but we can bypass that by defining each variable as a type. We will look at the in later in this article.

Install Typescript

There are two different installations for the project you can use-

Global Installation

In this, you install the Typescript as the global package. you can do that by simply running the following command in your terminal window-

npm install -g typescript

TypeScript in Your Project

When you install typescript for your projects such as for React or Angular then their typescript config file is required what kind of setting you want or not. Use the following command to install the typescript to your project-

npm install typescript --save-dev

For more info visit here

Types in Typescript

  • number

  • string

  • boolean

  • null

  • undefined

  • void

  • object

  • array

  • tuples

  • unknown

  • never

  • any (should not use)

and many more.

Situations to use typescript

For example, there is a function increaseScore and it takes currentScore and increases the score by increaseBy number and returns the updated score.

index.js

function increaseScore(currentScore, increaseBy){
	return currentScore + increaseBy;
}
 
increaseScore(10, 2) // output : 12
increaseScore(10, "2") // output : 102

If someone passes the string or other thing, then it will throw an error in the production or runtime. Such as in the second example where the score becomes 102 it's a bug as shown in the image.

Now, How can we prevent that using typescript? We will look at the later in depth. You can define the type of a variable like this-

let variableName: type = value;

Primitive Types

Primitive types in JavaScript are-

  • string

  • boolean

  • number

string

String values are surrounded by single quotation marks or double quotation marks. They are used to store text data.

index.ts

let player: string = "John";
 
// โœ… CORRECT
player = "anny";
 
// โŒ ERROR: Type 'number' is not assignable to type 'string
player = 4;

As you can see we assign the name (Anny) in line 3 and we assign the number in line 4 and it immediately throws an error. It's what typescript is. You don't need to run the typescript to get the error as JavaScript does.

boolean

In boolean it could be either true or false otherwise, it will throw you an error as shown in the following code-

index.ts

let isLoggedIn: boolean = false;
 
// โœ… CORRECT
isLoggedIn = true;
 
// โŒ ERROR: Type 'number' is not assignable to type 'boolean'
isLoggedIn = 5;
 
// โŒ ERROR: Type 'string' is not assignable to type 'boolean'.
isLoggedIn = "hello";

number

JavaScript does not have a special runtime value for integers, so thereโ€™s no equivalent to int or float - everything is simply number that's why in line 5 when we assign the price to 500.53 it doesn't give you an error because it's a number.

index.ts

let price: number = 200;
 
// ๐Ÿ‘‡โœ… CORRECT
price = 300;
price = 500.53;
 
// ๐Ÿ‘‡โŒ Error
price = false;
price = "3000";

Don't use any

So the question occurred that why Shouldn't we use any? The answer is simply because when you use any then you disable all the type checking for that variable and anyone can assign any kind of value to the variable. For example:

index.ts

// ๐Ÿ‘‡ Wrong Practice (by default 'any')
let hello;
 
// ๐Ÿ‘‡ 'hello' can take any type of value
hello =  2;
hello =  "world";
hello =  true;

On line: 1 we have not defined the type of the variable hello so its defaults as any and you can assign whatever you want as shown in the above example.

Now Imagine the scenario where you are calling an API and getting the data in the string format, but someone changes it to the boolean or number then your whole app functionality will crash due to that mistake. And Typescript prevents you from doing that. For example:

index.ts

let data; // type by default is any
 
function getData(){
	//.........API Call
	return  "Message";
}
 
data = getData(); 		// no issue because expected string

Example 2:

index.ts

let data;
 
function getData(){
	//.........API Call
	return 823;
}
 
data = getData(); 		// ISSUE: expected string but returns the number (won't throw error because type is `any`)

Solution:

index.ts

let data: string;
 
function getData(){
	//.........API Call
	return  "Message";
}
 
data = getData();

Now, if you pass something that is not a string then it will throw an error as shown below:

index.ts

let data: string;
 
function getData(){
  //........API Call
  return true;
}
 
// โŒ ERROR: Type 'boolean' is not assignable to type 'string'
data = getData();

You can useany whenever you donโ€™t want a particular value to cause type-checking errors.

Functions

Writing function is a piece of cake when you know the JavaScript but it gets a little bit complex in typescript. Donโ€™t worry, we will take every aspect of that. We need to define the types of two things in function Parameters & Return Types.

Parameters

Function parameters are the names listed in the function's definition. I'll take an old example that I've mentioned before:

index.ts

// This is a norma JavaScript Function that take two number and returns the sum 
// Problem is when you call the function you can pass any value 
// It won't show error because the variable does not have any kind of type
function increaseScore(currentScore, increaseBy){
	return currentScore + increaseBy;
}
 
// Now we define that both parameters have `number` type and it will only take the number
// otherwise it will throw an error
function increaseScore(currentScore: number, increaseBy: number) {
  return currentScore + increaseBy;
}

Following is an example of the error it will show when you pass the wrong value to the function:

index.ts

function increaseScore(currentScore:number, increaseBy:number){
	console.log(currentScore + increaseBy);
}
 
increaseScore(10, 2) // โœ… Correct
increaseScore(10,  "2");  	// โŒ Error
increaseScore(10,  [2,3,4])	// โŒ Error

Return Types

Return types matter. Because In typescript there are many return types. For example, you already know boolean, number and string. But the question here is how we defined which type should return from the function. You can do that by the following syntax.

index.ts

// Syntax
function funcName(para: paraType): returnType {
	//........
}
 
 
// For Example:
function greetings(name: string): string {
  return "hello" + name;
}
 
greetings("john");	// โœ…
greetings(true); 	// โŒ ERROR: Expected String
 
// greet is 'string' type because greetings() return stirng type
let greet = greetings("Don");
greet = 2;	 // โŒ ERROR: because type is 'string'

Other Types

void

void represents the return value of functions that donโ€™t return a value. Itโ€™s the inferred type any time a function doesnโ€™t have any return statements or doesnโ€™t return any explicit value from those return statements.

index.ts

// This function doesn't return anything thus its return type is void
function sayHi(name: string): void {
	console.log("Hi! " + name);
}

never

The never type represents values that are never observed. In a return type, this means that the function throws an exception or terminates the execution of the program.

index.ts

function handleError(errMsg: string): never {
  throw new Error(errMsg);
}

There are a lot more other types you can take a look at the documentation for further use.

Optional Parameters

When you define parameters, sometimes you don't need to pass the parameters. So for that, you can add ? next to the parameter as shown in the following code:

index.ts

function doSomething(num?: number) {
  // ...
}
doSomething();      // โœ… OK
doSomething(10);    // โœ… OK

Working with Object

In typescript working with objects could make you feel a little weird. Why? You will know why at the end of this section. There are many instances where you can use objects. Let's look at them one by one-

Passing Object as Parameter

Passing an object as a Parameter could be a little tricky. You need to define the types of each property you are passing as shown in the following code:

index.ts

// Destructuring an Object
function signUp({email, password}: {email: string, password: string}): void{
	console.log(email);
}
 
// You can also define the signUp function like the following
function signUp(user: {email:  string, password:  string}):  void{
	console.log(user.email);	
}
 
signUp();     // โŒ ERROR: need to pass an object
signUp({});   // โŒ ERROR: to pass an object with email & password ,
signUp({email: "hello@gmail.com", password: "12345678"}); // โœ… Correct

Now, what if you want to pass an object with more than these two parameters:

index.ts

function signUp(user: { email: string; password: string }): void {
  console.log(user);
}
 
// Passing name in the signUp function
// โŒ ERROR: 'name' does not exist
signUp({ email: "hello@j471n.in", password: "12345678", name: "Johnny" }); 
 
 
// Creating a separate object and then passing it with the name
// โœ… Correct and No Error, But if you use 'name' in the signUp function then you'll get an error
let newUser = { email: "hello@j471n.in", password: "12345678", name: "Johnny" };
signUp(newUser);

Returning Object from Function

You can return an object through many ways from a function some of them are shown in the following code along with whether is it correct or not.

index.ts

// โŒ ERROR: A function whose declared type is neither 'void' nor 'any' must return a value
// As function needs to return an object with name & age properties
function getInfo():{name:  string, age:  number}{}
 
// โŒ ERROR: Property 'age' is missing
// Function must have all the properties as specified (name, age)
// And It only returns the name that's why it throws an error
function getInfo():{name: string, age: number}{
  return {name: "John"};
}
 
// โœ… CORRECT 
// No Error Because all the things are correct
function getInfo():{name: string, age: number}{
  return {name: "John", age: 29};
}
 
// โŒ ERROR: 'lastName' does not exist  
// As function should return only 'name' and 'age'
// And it returns 'lastName' too
function getInfo():{name: string, age: number}{
  return {name: "John", age: 29, lastName: "Doe"};
}
 
// โœ… CORRECT 
// You can assign an object to some variable and then return it 
// Even if it has more properties as described
function getInfo():{name: string, age: number}{
  let user = {name: "John", age: 29, lastName: "Doe"}
  return user;
}
 
// โŒ ERROR: A function whose declared type is neither 'void' nor 'any' must return a value
// As you can see it has two {}
// First {} shows that it should return an object
// Second {} is function definition
// It should return an object
function getInfo():{}{}
 
// โœ… CORRECT 
// It returns and object that's why it works, It can have any properties because we haven't specified
function getInfo():{}{
  return {name: "John"}
}

The above code example might be scary to look at but we can achieve these things with Type as well. We'll look at it in the next section of this article.

Type Aliases

We can use the objects as shown in the previous example but what if there are 10+ functions that need the same data of the user then you'll be typing for all of them separately that's where Type comes into play. Let's look at the old example and how we can use type to make it reusable.

index.ts

type User = {
  email: string;
  password: string;
};
 
// โœ… CORRECT
// Passsing Object Type Aliases
function signUp(user: User){}
 
// โœ… It's the correct way to call
signUp({email: "some@hello.com", password: "1233"})
 
// โŒ ERROR : 'name' does not exist in type 'User'
signUp({email: "some@hello.com", password: "1233", name: "Sam"})
 
// โœ… You can pass extra information by using a variable
let userObj =  {email:  "some@hello.com", password:  "1233", name:  "Sam"}
signUp(userObj)

You can use a type alias to give a name to any type at all, not just an object type. For example:

type ID = number;
let userId: ID = 111; // โœ… CORRECT

I guess you get the point that we use type to define the actual type of the variable. It can be used anywhere.

Readonly and Optional

readonly means that the user or anyone cannot manipulate that variable, for example id optional means that these parameters are optional which we have looked at before in functions.

Let's take an example of User where the user will have three properties id, email, and name.

index.ts

// Defining the User type
type User = {
	readonly id : string,
	name: string,
	email: string,
}
 
// Now Let's create a user variable using the above type:
let myUser:User = {
	id : "3947394",
	name: "harry",
	email: "h@harry.com",
}
 
// Now I'll assign the values to the myUser object
 
// โœ… CORRECT
myUser.name = "Potter";
myUser.email = "hello@harry.com";
 
// โŒ ERROR: Cannot assign to 'id' because it is a read-only property
myUser.id = "121324";

Now, let's take a look at the optional:

index.ts

// Defining the User type
type User = {
	readonly id : string,
	name: string,
	email: string,
	dob?: string 	// optional
}
 
// โœ… CORRECT
let user1: User = {
	id : "3947394",
	name: "harry",
	email: "h@harry.com",
	dob: "02/12/1998"
}
 
// โœ… CORRECT
let user2: User = {
	id : "3947394",
	name: "harry",
	email: "h@harry.com",
}

Intersection in type

You can combine two or more types using &. You can do that as shown in the following code:

index.ts

type User = {
	readonly id : string,
	name: string,
	email: string,
}
 
type Admin = User & {
	key: string, 
}
 
// You can use Admin like this:
let newAdmin: Admin = {
  id: "3KD5D874",
  name: "Lucifer",
  email: "lucifer@hell.com",
  key: "Oh my me!",
};

Now Admin will have User properties as well as shown in the above code.

Arrays

To create an Array of a certain type there is a special syntax for that:

index.ts

let variableName: arrayType[] = []
 
// For example: 
let num: number[] = [1,2,3]
let name: string[] = ["sam", "john"]

As shown in the above example that you can define the array's type by using the type followed by square brackets (number[])

This is simple right? Let's look at some other concepts. Let's look at the Dos and Don'ts of an Array:

index.ts

// โŒ DON'T 
// If you define an array like this then the type will be 'never'
// which means you can't push anything
let num:[] = []
num.push(1) // ERROR
 
 
let num:number[] = []
num.push(1)
num.push("232")  // โŒ ERROR : rgument of type 'string' is not assignable
 
 
let names:string[] = []
names.push("john")
names.push(1) 	// โŒ ERROR : rgument of type 'number' is not assignable
 
/* -----------Let's Add Objects into Array-------- */
type User ={
	name: string,
	email: string,
}
 
// โœ… That's how you can deine an array of objects
const allUsers: User[] = [];
 
// โœ… CORRECT
allUsers.push({name: "sam", email:"sam@hello.com"})
// โŒ ERROR: email property missing
allUsers.push({name: "sam"})
// โŒ ERROR: missing name & email
allUsers.push({})

The above example shows how you can use Array. There is one more way to create an Array:

index.ts

let newArray: Array<number> = []
let nameArray: Array<string> = []
let allUsers: Array<User> = []

It means that creating an Array of the defined type in <>.

Readonly Array

The ReadonlyArray is a special type that describes arrays that shouldnโ€™t be changed.

index.ts

let players: ReadonlyArray<string> = ["ronaldo", "messi"]
// or
let  players: readonly string[] = ["ronaldo", "messi"];  
 
// โŒ Can't do that
players[0] = "jordon";
players.push("shaq")
 
// โœ… Can do 
console.log(players[0]);

In the above code, there are two methods from which you can define the readonly Array.

At the End of this section, I want to add one more thing to this. What if we want a nested Array means Array inside an array? You can do that as shown in the following code:

index.ts

const cords: number[][] = [
	[22, 55],
	[45, 22]
]
 
const data: number[][][] = [
	[[22], [25, 65]],
	[[99, 34, 56], [12, 9]],
]

You can define the nested array as shown in the above code. You can nest as many arrays as you want. And if you want them to be different types then you need to create a type for them.

Union

In Typescript, you can define more than one type of variable. You can do that by just putting (|) in the middle of two types. Let me show you what I am saying:

index.ts

let id: string | number;
 
id = 123;		 	// โœ…
id = "34398493";	// โœ…

Both the example shown in the above code is correct. Let's take a little more complex example to understand how it works:

index.ts

type User = {
	name: string,
	id : number
}
 
type Admin ={
	username: string,
	id: number,
	key: number,
}
 
let newUser : User | Admin;
 
// โœ… CORRECT
newUser = {name: "John", id: 123};
newUser = {username : "j333", id : 123, key: 345};
newUser = {name: "j333", id : 123, key: 345};
 
// โŒ ERROR: Property 'key' is missing
newUser = {username : "j333", id : 123};

Union in Functions

You can also use these with function parameters as shown below:

index.ts

// โœ… It works and id have string and number type
function setUserId(id: string | number){
	console.log(id);
}
 
// โŒ ERROR : 
// What if we do like this:
function setUserId(id: string | number ){
	id.toLowerCase()  // โŒ ERROR: Property 'toLowerCase' does not exist on type 'number
}

Now the above code will show you an error because id has two types string and number. The string can be converted to lowercase but the number can't. That's why it is showing you the error. You can use the conditional statement to do your desired stuff. Such as:

index.ts

function setUserId(id: string | number ){
	if (id === "string") {
		id.toLowerCase()
	}
	// do other things
}

Now it won't give you any errors because we are making sure that toLowerCase() only calls when the id is a string.

Union in Array

In the array section, we discussed that we can set an array type with two methods. But the issue with that is What if we want an array that has multiple types of values such as string and number then what do we do? The answer is Union. Now let's take a look at how you do it:

index.ts

// You might be thinking like this
// Nah โŒ you can't do that
let newArray: number[] | string[] = [1,2, "hello"];
 
// โœ… Correct way to define the array with multiple type is:
let newArray: (number | string)[] = [1,2, "hello"];

Special Case for Union

Now imagine you are working on CSS Framework and you are designing a Button. And you are asking the user to tell what size should button have. Now you can use the variable type as string but the user can type anything, right? Let's understand this via an example:

index.ts

// โŒ WRONG APPROACH
let ButtonSize: string;
 
ButtonSize = "big";
ButtonSize = "medium";
ButtonSize = "don't know";  // that's why don't use this
 
// โœ… CORRECT APPROACH
let ButtonSize : "sm" | "md" | "lg" | "xl";
 
ButtonSize = "sm";
ButtonSize = "md";
ButtonSize = "lg";
ButtonSize = "xl";
 
// โŒ ERROR: Type 'large' is not assignable to type
ButtonSize = "large";

As you saw at the end of the above code the typescript restricts you from assigning large to ButtonSize. It only accepts special types defined by you for the button.

Tuples

Tuple types are a type of array of known length where the different elements may have different types. A value of type [number, string] is guaranteed to have a length of 2, with a number at element 0 and a string at element 1. Let's look at the example:

index.ts

let x: [number, string];
 
x = [110, "Mohan"]; // โœ… CORRECT 
x[0] = 120; // โœ… CORRECT 
x = ["Mohan", 110];	// โŒ ERROR: Initialize it incorrectly

Let's take another example of RGB. As you already know RGB takes three values that should be numbered.

index.ts

let rgb: [number, number, number];
 
rgb = [225, 224, 10]; 		// โœ… CORRECT
rgb = [225, 224, "10"];		// โŒ ERROR: type 'string' is not assignable to type 'number'
rgb = [225, 224, 10, 40];	// โŒ ERROR : Source has 4 element(s) but target allows only 3

Issue with Tuples

Tuples sound great right? But they have a major flaw. TypeScript allows you to call methods like push(), pop(), shift(), unshift(), and splice() on values of tuple types.

If you don't understand what I am saying then let me show you with code example. I am taking the old example of RGB:

index.ts

let rgb: [number, number, number];
 
// โœ… This is correct because it has all the values
rgb = [225, 224, 10];
 
// โŒ ERROR : Source has 4 element(s) but target allows only 3
// It is not allowed Right.
rgb = [225, 224, 10, 40];	
 
// Now let's do this:
rgb.push(50)
console.log(rgb)  // output: [225, 224, 10, 50]
// This is the flaw.

You can apply any Array method to Tuple. That's why it destroys the supposed guarantees of tuple types.

Don't only rely on Tuples. Use only when necessary.

Enums

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.

Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

Numeric enums

We will first take a look at Numeric enums and how we can create them. An enum can be defined using the enum keyword.

index.ts

enum Direction  {
	Up, 
	Down, 
	Left, 
	Right,
}

Above, we have a numeric enum where Up is initialized with 0. All of the following members are auto-incremented from that point on. In other words, Direction.Up has the value 0, Down has 1, Left has 2, and Right has 3.

In the Numeric enums, the values are in the incremented order as explained above. You can manipulate these values as you want. Let's take a few examples of that:

index.ts

// Up = 1, Down = 2, Left = 3, Right = 4 
enum Direction  {
	Up = 1, 
	Down, 
	Left, 
	Right,
}
 
// Up = 1, Down = 5, Left = 6, Right = 7
enum Direction  {
	Up, 
	Down = 5, 
	Left, 
	Right,
}
 
// Up = 10, Down = 11, Left = 14, Right = 15
enum Direction  {
	Up = 10, 
	Down, 
	Left = 14, 
	Right,
}

In the above code example, I have updated the values and shown you what will be the value of the others members.

String enums

String enums are a similar concept. In a string enum, each member has to be constant-initialized with a string literal, or with another string enum member.

index.ts

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

To access any members you can do the following:

index.ts

console.log(Direction.Up) // output: UP

Heterogeneous enums

enums can be mixed with string and numeric members, but itโ€™s not clear why you would ever want to do so. itโ€™s advised that you donโ€™t do this.

index.ts

enum ExtraFries {
  No = 0,
  Yes = "YES",
}

Interface

An interface declaration is another way to name an object type. You can create it by using the interface keyword:

index.ts

interface User {
	name: string,
	age: number,
}
 
// โœ… CORRECT
let newUser : User = {name: "John", age: 28};
// โŒ ERROR: property 'age' is missing
let newUser : User = {name: "John"};
// โŒ ERROR: missing the following properties from type 'User': name, age 
let newUser : User = {};

You can also use readonly and optional approach in interface:

index.ts

interface User {
	readonly id: number 	// readonly variable
	name: string,
	age: number,
	specialKey? : string, 	// optional 
}

You can also pass functions to the interface there are two methods you can do that:

index.ts

// Method-1
interface User {
	getDiscount(coupon: string): number    	
}
 
// For Both you need to call this like this:
const newUser: User = {
	getDiscount: (coupon: "KIJ298DD9J")=>{
		return 10;
	}
}
 
// Method-2
interface User {
	getDiscount: (coupon: string) => number
}
 
// ๐Ÿ‘‰ You see I have changed the 'coupon' to 'couponName'
// You don't need to match the name of the parameter here
// It will take care of it
const newUser: User = {
	getDiscount: (couponName: "KIJ298DD9J")=>{
		return 10;
	}
}

In Method 1 you can simply use the () to say it functions like: getDiscount(): number and string is the return type and it takes no arguments.

In Method 2 we use Arrow Function like getDiscount: () => number.

Interface vs Type

Type aliases and interfaces are very similar, and in many cases, you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface that is always extendable.

Let's Differentiate them with a few examples:

ex1

ex2

Classes

As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.

index.ts

// Hereโ€™s the most basic class - an empty one:
class Cords {}

Now Let's create a class with some variable fields:

index.ts

class Cords {
	x: number;
	y: number;
}
 
// โŒ ERROR: Property 'x' has no initializer and is not assigned in the constructor

you might be thinking that the above code is correct that we define the x and y But the issue here you need to define the default number if you are not using constructor. The correct one would be:

index.ts

class Cords {
	x: number = 0;
	y: number = 0;
}
 
// โœ… CORRECT

You can access them and change the value and do whatever you want. Let me show you:

index.ts

class Cords {
	x: number = 0;
	y: number = 0;
}
 
const coordinates = new Cords();
 
// โœ… CORRECT
coordinates.x = 4;
coordinates.y = 10;
 
// โŒ ERROR: Type 'string' & 'boolean' are not assignable to type 'number'
coordinates.x = "11";
coordinates.y = false;

Typescript makes sure that if you are changing some values then the types should be correct. You can also restrict the user from changing something by using the readonly keyword:

index.ts

class Cords {
	x: number = 0;
	y: number = 0;
	readonly quadrant: number = 4;
	
}
 
const coordinates = new Cords();
 
// โœ… CORRECT
console.log(coordinates.quadrant)
 
// โŒ Cannot assign to 'quadrant' because it is a read-only property
coordinates.quadrant = 2;

constructor

Earlier I said-

we define the x and y But the issue here you need to define the default number if you are not using constructor.

let's do that with the constructor:

class Cords {
	x: number;
	y: number;
 
	constructor(x:number, y: number){
		this.x = x;
		this.y = y;
	}
}
 
// ๐Ÿ‘‡ using Cords Class
const coordinates =  new  Cords(10,  20);

As you can see in the above code you don't need to set the default value for x and y because we are using. But what if we are not taking a single value from the constructor? For instance:

index.ts

class Cords {
	x: number;
	y: number;
	quadrant: number; // โŒ ERROR: Property 'quadrant' has no initializer and is not assigned in the constructor.
 
	constructor(x:number, y: number){
		this.x = x;
		this.y = y;
	}
}

So we can't do that. If we are not taking value from constructor then we need to assign the default value to the variable.

One more thing, you cannot remove the top part where you define x and y otherwise, it will give you an error that Property 'x' does not exist on type 'Cords'

index.ts

// โŒ WRONG 
class Cords {
	constructor(x:number, y: number){
		this.x = x; 	// โŒ ERROR: Property 'x' does not exist on type 'Cords'
		this.y = y; 	// โŒ ERROR: Property 'Y' does not exist on type 'Cords'
	}
}
 
// โœ… CORRECT
class Cords {
	x: number;
	y: number;
	
	constructor(x:number, y: number){
		this.x = x; 	
		this.y = y; 	
	}
}

That's how you can use constructor in Typescript.

Private and Public

You might have heard that before about private and public somewhere. private doesnโ€™t allow access to the member.

index.ts

class Cords {
	x: number = 0;
	y: number = 0;
	private z: number = 0;  // ๐Ÿ‘ˆ private
 
	constructor(x:number, y: number){
		this.x = x; 	
		this.y = this.y;
		this.z = 10; // โœ… CORRECT: because we can use it inside this class 
	}
}
 
const cds = new Cords(10, 20);
 
cds.z; // โŒ ERROR: Property 'z' is private and only accessible within class 'Cords'

If you want any property to be private you need to use the private keyword before that property as shown in the above code. Properties x and y will be public and anyone can access them. If you want you can also use the public keyword before them but it's not necessary because by default they are public.

There is another way to use them. Let's take a look:

index.ts

// โœ… Here user passes 'x' and 'y' but not 'z'
// So you need to define 'z' separately as private
class Cords {
	private z: number = 10;
	constructor(public x:number, public y: number){
		// ..........
	}
}
 
// โœ… Here user passes 'x' and 'y' but 'y' is 'private'
// You don't need to define them before this is another way to do it.
// If you are defining like that then you need to use 'public' or 'private'
class Cords {
	constructor(public x:number, private y: number){
		// ..........
	}
}

Getters and Setters

Accessor properties are methods that get or set the value of an object. For that, we use these two keywords:

  • get - to define a getter method to get the property value

  • set - to define a setter method to set the property value

We will take a different example now to understand Getters and Setters in detail:

index.ts

class Dish {
	dishName: string;
	private _chef = "Gordon Ramsay"
	
	constructor(dishName: string){
		this.dishName = dishName;
	}
 
	get chefName(): string{
		return this._chef;
	}
 
	set updateChefName(name: string) {
		this._chef = name;
	}
}
 
const dish1 = new Dish("Tuna");
 
dish1._chef  // โŒ ERROR: Property '_chef' is private and only accessible within class 'Dish'
console.log(dish1.chefName) // โœ… Output: Gordon Ramsay
 
dish1.updateChefName  = "James Oliver"; // Using `setter` to update the _chef 
console.log(dish1.chefName) // โœ… Output: James Oliver

There is one thing to remember that set accessor cannot have a return type. If you pass some return type such as void or never then it will throw an error:

index.ts

// โŒ  ERROR: A 'set' accessor cannot have a return type annotation.
set updateChefName(name: string):void {
	this._chef = name;
}
 
// โŒ  ERROR: A 'set' accessor cannot have a return type annotation.
set updateChefName(name: string):never {
	this._chef = name;
}
 
// โœ…  CORRECT: No return type
set updateChefName(name: string) {
	this._chef = name;
}

protected

protected members are only visible to subclasses of the class theyโ€™re declared in. When you use private then it won't allow access to the member outside its defined class not even subclasses but with protected you can access that member in subclasses but not outside that scope:

index.ts

class Parent {
  private firstName: string = "Johnny"; // ๐Ÿ‘ˆ PRIVATE
  protected lastName: string = "Deep";  // ๐Ÿ‘ˆ PROTECTED
}
 
class Child extends Parent {
	middleName: string = "foo";
}
 
const d = new Child();
 
// โŒ ERROR: Property 'firstName' is private and only accessible within the class 'Parent'
d.firstName; 
// โŒ ERROR: Property 'lastName' is protected and only accessible within class 'Parent' and its subclasses
d.lastName; 
// โœ… CORRECT
d.middleName;

In the above example, you saw that private and protected members cannot access outside their scope.

Now let's just try to access them in the subclass Child:

index.ts

class Parent {
  private firstName: string = "Johnny";
  protected lastName: string = "Deep";
}
 
class Child extends Parent {
	// โŒ ERROR: Property 'firstName' is private in type 'Parent' but not in type 'Child'.
	firstName = "samu"; 	
	// โœ… CORRECT: Because 'protected' allows you to manipulate 'lastName' in subclasses too	
	lastName = "Walker"; 	
}

So basically if you want to use members in subclasses then use protected else use private. These also work on methods defined inside the class:

index.ts

class Counter {
  private _count: number = 0;
 
  private increaseCounter(){
	this._count += 1;
  }
 
  protected decreaseCounter(){
	this._count -= 1;
  }
}
 
class SubCounter extends Counter {
	updateCounter(){
		// โŒ Property 'increaseCounter' is private and only accessible within class 'Counter'.
		this.increaseCounter()
		this.decreaseCounter(); /// โœ… because protected
	}
}
 
const c = new SubCounter();
 
// โŒ Property 'increaseCounter' is private and only accessible within class 'Counter'.
c.increaseCounter()
 
// โŒ Property 'decreaseCounter' is protected and only accessible within class 'Counter' and its subclasses.
c.decreaseCounter()

implements

You can use an implements clause to check that a class satisfies a particular interface. An error will be issued if a class fails to correctly implement it:

index.ts

interface TakePhoto {
	exposure: number;
	cameraMode: string;
	flashLight: boolean;
}
 
// โŒ ERROR: Class 'Snapchat' incorrectly implements interface 'TakePhoto'.
// โŒ ERROR: Type 'Snapchat' is missing the following properties from type 'TakePhoto': exposure, cameraMode, flashLight
class Snapchat implements TakePhoto{}
 
// โœ… CORRECT: We have passed all the properties
// You can add more properties if you want such as 'flilter'
class Snapchat implements TakePhoto{
	exposure: number = 0;
	cameraMode: string = "normal";
	flashLight: boolean = false;
	
	// this property is not in 'TakePhoto' interface
	flilter: string = "none"
}
 
// โœ… CORRECT: You can use 'constructor' too
class Snapchat implements TakePhoto{
	constructor(
		public exposure: number,
		public cameraMode: string,
		public flashLight: boolean,
	){}
}

As you can see in the above code. I have defined an interface TakePhoto and then I am using that interface to create SnapChat class. But implements makes sure that all the properties that are defined in TakePhoto interface should also be defined in Snapchat classes. You can add more properties if you want but it must include TakePhoto properties.

These conditions also work for methods. If you defined any method in interface and used that to create a class then your class must include that method:

index.ts

interface TakePhoto {
	clickPhoto():boolean;
}
 
// EXAMPLE-1
// โœ… CORRECT
class Snapchat implements TakePhoto{
	clickPhoto(){
		return true;
	}	
}
 
// EXAMPLE-2
// โŒ ERROR: Property 'clickPhoto' is missing in type 'SnapChat' but required in type 'TakePhoto'.
class Snapchat implements TakePhoto{
	applyFilter(){}
}

In Example-1 we added the clickPhotomethod in Snapchat class, then it will work fine. But in Example-2 we didn't add that method instead, we added another method applyFilter which won't affect the class. But not adding clickPhoto will give you the error.

abstract classes and Members

In typescript Classes, methods and fields can be abstract.

The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract members. When a class doesnโ€™t have any abstract members, it is said to be concrete.

Let's define an abstract class:

index.ts

abstract class TakePhoto {
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
}

In the above code, I have created a simple abstract class. Now you'll say it looks the same as the normal class except for the abstract keyword. You are right. But now let's create an instance of this class let's see what happens:

index.ts

abstract class TakePhoto {
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
}
 
// โŒ ERROR: Cannot create an instance of an abstract class.
const camera = new TakePhoto(0, "normal");

It doesn't allow us to create an instance of TakePhoto class. Now the question could be How we will use this class then. The answer is using extends. Let me show you what I mean:

index.ts

 
abstract class TakePhoto {
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
}
 
// Extending Snapchat Class with TakePhoto
class Snapchat extends TakePhoto{
}
 
// โœ… CORRECT
const camera = new Snapchat(0, "normal");

The above code is fully functional and you can use all the functionality of TakePhoto class using Snapchat. Let's take some more examples to understand abstract classes better:

index.ts

abstract class TakePhoto {
	abstract clickPhoto():void   // ๐Ÿ‘ˆ abstract method
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
}
 
// โŒ Error: Non-abstract class 'Snapchat' does not implement inherited abstract member 'clickPhoto' from class 'TakePhoto'
// abstract class forces you to implement that method
class Snapchat extends TakePhoto {
}
 
// โœ… CORRECT
// because 'Instagram' class contains 'clickPhoto' method
class Instagram extends TakePhoto{
	clickPhoto(){
		// do something
	}
}

In the above code, clickPhoto is an abstract method and the subclass must include that method because it is an abstract method. No, you'll be like Mannn......... you can do this using implements and interface. You are correct. In the above scenario, we can use implements and interface to create similar functionality.

Let's look at the following case which shows what makes abstract classes different:

index.ts

abstract class TakePhoto {
	abstract clickPhoto():void  // ๐Ÿ‘ˆ Abstract method
	
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
 
	// ๐Ÿ‘‡ Normal method
	applyFilter(name: string): void { 
		// apply given filter
	}
}
 
// โœ… CORRECT
class Instagram extends TakePhoto{
	clickPhoto(){
		// do something
	}
}
 
 
// Creating an instance of Instagram
const user = new Instagram(0, "normal");
 
// โœ… It works because it is already defined in 'TakePhoto' class
user.applyFilter("infrared")

As you can see in the above code that TakePhoto has a new method applyFilter and it is not defined in the subclass Instagram and an instance of Instagram class is calling that method. You cannot do that in interface. In abstract classes both (clickPhoto & applyFilter) can co-exist but you cannot implement any method in interface.

super()

Just as in JavaScript, if you have a base class, youโ€™ll need to call super(); in your constructor body before using any this. members. Let's take one example:

index.ts

class Base {
  k = 4;
}
 
class Child extends Base {
  constructor() {
	// ๐Ÿ‘‡โŒ ERROR: 'super' must be called before accessing 'this' in the constructor of a derived class.
    console.log(this.k);
    super();
  }
}

In the above code, I am trying to access the k from the Base class but before calling super() so typescript will throw you an error. You should use super() before accessing k.

Let's take another example that we used earlier to TakePhoto:

index.ts

// Simple abstract Class
abstract class TakePhoto {
	constructor(
		public exposure: number,
		public cameraMode: string,
	){}
}
 
// โŒ ERROR: Constructors for derived classes must contain a 'super' call
class Instagram extends TakePhoto{
	constructor(
		public exposure: number,
		public cameraMode: string,
	){} 
}
 
 
// โœ… CORRECT WAY
class Instagram extends TakePhoto{
	constructor(
		public exposure: number,
		public cameraMode: string,
	){
		super(exposure, cameraMode);
	}
}

Now you might think what if we want to pass more than these two arguments to Instagram class then how do we do that it's simple:

index.ts

// โœ… CORRECT WAY with another argument
class Instagram extends TakePhoto{
	constructor(
		public exposure: number,
		public cameraMode: string,
		public filter: string
	){
		super(exposure, cameraMode);
	}
}
 
 
// Creating an instance of Instagram
const user = new Instagram(0, "normal", "mask");

That's all you need to use super()

Generics

TypeScript Generics is a tool that provides a way to create reusable components. It creates a component that can work with a variety of data types rather than a single data type. Let's see the problem first:

index.ts

function identityOne(value:  number):number {
	return value;
}

In the above function, we are taking a number and returning the same data type (number). Now, what if we want that whatever we pass to this function it will return the same data type? You might be thinking to use any. Let's try that too.

index.ts

function identityTwo(value: any): any{
	// some calculation... and returning string...
	return "hello";
}
 
identityTwo(2);

In identityTwo we pass number and we expect it to return a number however for identityTwo return type is any so it can return anything as we are returning string instead of number and Typescript won't give you an error. But we expected number, not string. Right? That's where generics come into play. They make sure that whatever type you pass should return the same type.

In generics, we need to write a type parameter between the open (<) and close (>) brackets, which makes it a strongly typed collection.

index.ts

// ๐Ÿ‘‡ That's how you use generics
function identityThree<Type>(value: Type): Type{
	return value;
}
 
identityThree(3);
identityThree("hello")

Now you can pass any variable identityThree to this and it will give you the same data type.

In generics some people use <T> short for <Type> you can use whatever you want.

Now there is one more concept I would like to mention. What if we are defining our own types and we expect it to return the same? For example:

index.ts

// Defining Laptop interface
interface Laptop {
  CPU: string;
  cores: number;
  RAM: number;
}
 
// Using Laptop interface using generics
function identityFour<Laptop>(value: Laptop): Laptop {
  return value;
}
 
identityFour<Laptop>({});  // โŒ ERROR: '{}' is missing the properties
identityFour<Laptop>({
  CPU: "intel",
  cores: 16,
  RAM: 12,
}); // โœ… CORRECT

As you can see in the above example when you call identityFour the function you had to use <Laptop> before (). It's because Laptop is not the known type as number and string etc. So you need to do that in order to call this function.

Generics using the Arrow function

You might be thinking it's all good but how can we define generics using the arrow function as many people use the Arrow function? So let me show you the example with the arrow function (I'll take the same previous example of Laptop):

index.ts

// Using the' function' keyword
function identityFour<Laptop>(value: Laptop): Laptop{
	return value;
}
 
// using arrow function
const identityFour = <Laptop,>(value: Laptop): Laptop => {
	return value;
}

It looks complex but it's not. First, you type the name of the function with const then use assign it <Laptop,> (, says that it's not a JavaScriptX or HTML tag it's generics. If you remove , you might get errors. But you don't use , when you use the define function use function keyword) then use (value: Laptop): Laptop to define the parameter type and the return type for the function and after that just use => {}. Now it's simple right?

In generics, some people use <T> short for <Type> you can do whatever you want.

Generics in Array

Generics can be used in Array as well. Look at the following example where we want to find the product in the array:

index.ts

function findProduct<T>(products: T[]): T{
	// some searching you get the index 5
	const index = 5;
	return products[index];
}

In the above function, we use <T> means that we are using generic, and T[] means that it's an array of types (it could be anything array of objects, the array of strings) and then we return the single item of that array which would be the same type as T

If it doesn't make any sense. Take an example of an array of objects (T[]). and then you pass that array to findProduct and then it performs some operation to give you a single result it will be an object (T) that's why it won't give any error because it expects you to return that object.

Generic Classes

Generic classes have a generic type parameter list in angle brackets (<>) following the name of the class.

index.ts

interface Laptop {
	price: number,
	cpu: string,
	ram: number,
}
 
interface CellPhone {
	price: number, 
	ram: number, 
	screenSize: string,
}
class PurchaseDevice<Type>{
	public cart: Type[] = [];
	addToCart(product: Type) {
		this.cart.push(product);
	}
}

Let me walk you through with code we have two interfaces Laptop and CellPhone the main difference between these two is that Laptop has cpu property and CellPhone has screenSize.

And after that, we have a brand new class PurchaseDevice and this is a very simple class but there is one different thing in this class which is <Type> this is generics. It says that whatever user passes in <Type> uses that to create a cart array and then addToCart the function adds those items to the cart.

Let's take the above code and implement the functionality:

index.ts

const newLaptop = new PurchaseDevice<Laptop>();
 
// โœ… CORRECT
newLaptop.addToCart({ price: 1000, cpu: "i9", ram: 12 });
// โŒ ERROR: 'screenSize' does not exist in type 'Laptop'
newLaptop.addToCart({ price: 1000, cpu: "i9", screenSize: "1920x1080" });

In the above code, we use PurchaseDevice but the difference here is <Laptop> and it will make sure that when you call addToCart then you must need to pass the object that has Laptop properties as shown in the above example. You can do the same with the CellPhone interface as well.

Narrowing

Since a variable of a union type can assume one of several different types, you can help TypeScript infer the correct variable type using type narrowing. To narrow a variable to a specific type, implement a type guard. Use the typeof operator with the variable name and compare it with the type you expect for the variable.

index.ts

const choices: [string, string] = ["NO", "YES"];
const processAnswer = (answer: number | boolean) => {
  if (typeof answer === "number") {
    console.log(choices[answer]);
  } else if (typeof answer === "boolean") {
    if (answer) {
      console.log(choices[1]);
    } else {
      console.log(choices[0]);
    }
  }
};
processAnswer(true); // Prints "YES"
processAnswer(0); // Prints "NO"

The above function can take two types of values it could be number or boolean so we need to make sure that in both of the cases the answer should be accurate. It only happens when you use union types in which users can pass different types of data. To generate the correct result you need to check the type before performing any operation. One more example Could be this:

index.ts

// โŒ ERROR: Property 'toUpperCase' does not exist on type 'number'
function anotherFunction(value: number | string) {
  value.toUpperCase();
}
 
// โœ… CORRECT
function anotherFunction(value: number | string) {
  if (typeof value === "string") {
    value.toUpperCase();
  } else if (typeof value === "number") {
    value += 3;
  }
}

It converts the string to uppercase and adds 3 to the value if it is a number. So we need to check every aspect if we are using union type.

TypeScript Type Guard

Type guard is just a fancy word for type-checking of the variable or property. It can be implemented with the typeof operator followed by the variable name and compare it with the type you expect for the variable.

index.ts

if (typeof age ===  'number')  {
	age.toFixed();
}

Thein operator narrowing

If a variable is a union type, TypeScript offers another form of type guard using the in operator to check if the variable has a particular property.

index.ts

interface User {
  name: string;
  email: string;
}
 
interface Admin {
  name: string;
  email: string;
  isAdmin: boolean;
}

Now we will create a function to check whether the account is of admin or not:

index.ts

// โŒ WRONG
function isAdminAccount(account: User | Admin) {
  return account.isAdmin; // โŒ roperty 'isAdmin' does not exist on type 'User'
}

We can't just use . notation to get the isAdmin property because User doesn't have it. That's why it is showing an error.

index.ts

// โœ… CORRECT
function isAdminAccount(account: User | Admin) {
  if ("isAdmin" in account) {
    return account.isAdmin;
  }
  return false;
}

We need to check if the "isAdmin" is in account and then return the value otherwise simply return false because it would be the user's account.

instanceof narrowing

JavaScript has an operator for checking whether or not a value is an โ€œinstanceโ€ of another value or another class. As you might have guessed, instanceof is also a type guard.

index.ts

function logValue(x: Date | string) {
  if (x instanceof Date) { // ๐Ÿ‘ˆ using instanceof
    console.log(x.toUTCString());
  } else {
    console.log(x.toUpperCase());               
  }
}

The above code simply checks if the x is an instance of a Date or not. We can create our own class and then check it:

index.ts

function logValue(x: Animal | string) {
  if (x instanceof Animal) {
    console.log("yes");
  } else {
    console.log("its just a string....");
  }
}
 
class Animal {
  name: string = "Bruno";
}
const dog = new Animal();
logValue(dog); // "yes"

Using type predicates

Weโ€™ve worked with existing JavaScript constructs to handle narrowing so far, however, sometimes you want more direct control over how types change throughout your code.

To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:

index.ts

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature.

Discriminated unions

Most of the examples weโ€™ve looked at so far have focused on narrowing single variables with simple types like string, boolean, and number. While this is common, most of the time in JavaScript weโ€™ll be dealing with slightly more complex structures.

letโ€™s imagine weโ€™re trying to encode shapes like circles and squares. Circles keep track of their radiuses and squares keep track of their side lengths. Weโ€™ll use a field called kind to tell which shape weโ€™re dealing with. Hereโ€™s a first attempt at defining Shape.

index.ts

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  side: number;
}
 
type Shape = Square | Circle;

Now let's create a function that will handle this shape:

index.ts

function handleShape(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
  return shape.side * shape.side;
}

The same checking works with switch statements as well. Now we can try to write our complete getArea without any pesky ! non-null assertions.

index.ts

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;              
  }
}

The never type

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldnโ€™t exist.

Exhaustiveness checking

The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.

For example, adding a default to our getArea function which tries to assign the shape to never will raise when every possible case has not been handled.

index.ts

type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Adding a new member to the Shape union will cause a TypeScript error:

index.ts

// ๐Ÿ‘‡ Adding new Interface Triangle
interface Triangle {
  kind: "triangle";
  sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      // โŒ Error: Type 'Triangle' is not assignable to type 'never'
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Conclusion

This is just an introduction to typescript. You can dive deep into typescript by at a look at its documentation. Now you'll be able to use typescript in any simple project. We also use Typescript in React or Angular. That's for another time.

Jatin's Newsletter

I write monthly Tech, Web Development and chrome extension that will improve your productivity. Trust me, I won't spam you.

Share on Social Media: