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
> 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 use
any
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:
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
andy
But 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 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 clickPhoto
method 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.