Table of Contents
Learn Typescript from Scratch
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.

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
> NaNThis 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 typescriptTypeScript 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-devFor 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 : 102If 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 stringExample 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 use
anywhenever 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]) // โ ErrorReturn 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); // โ
OKWorking 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"}); // โ
CorrectNow, 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; // โ
CORRECTI 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 incorrectlyLet'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 3Issue 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: UPHeterogeneous 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:


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 constructoryou 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;
}
// โ
CORRECTYou 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
xandyBut the issue here you need to define the default number if you are not usingconstructor.
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 OliverThere 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,
}); // โ
CORRECTAs 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.
