Typescript
TypeScript is a programming language that is a superset of JavaScript. It is maintained by Microsoft. TypeScript adds optional static typing and class-based object-oriented programming to JavaScript. This can help make code easier to read and debug. TypeScript is widely used in large-scale applications, especially in the Angular framework. It is also becoming increasingly popular in the Node.js community.
There are several benefits to using TypeScript over plain JavaScript:
- Type checking: TypeScript can perform static type checking, which can help catch errors before you run your code. This can make it easier to catch bugs and prevent problems when your code is running.
- Improved code readability and organization: TypeScript supports object-oriented programming, which can make it easier to organize and understand your code. This can be especially helpful for large-scale applications.
- Improved tooling: Because TypeScript is a superset of JavaScript, you can use existing JavaScript libraries with it. Additionally, many IDEs and text editors have built-in support for TypeScript, which can make it easier to write and work with your code. These tools often include features like code navigation and auto-complete, which can help you write and navigate your code more efficiently.
TypeScript can help improve the quality of your code and make it easier to work with large-scale applications. It can also provide a more structured and predictable way to develop JavaScript applications.
TypeScript Introduction
-
Basic Syntax
Basic Syntax
Variables
TypeScript is a programming language that is a superset of JavaScript, meaning that it includes all of the capabilities of JavaScript but also adds some additional features. One of these features is the ability to specify the type of a variable when it is declared.
In JavaScript, variables are declared using thelet
keyword, and the type of the variable is determined by the value that is assigned to it. For example:let x = 5; // x is a number let y = "hello"; // y is a string
You can specify the type of a variable when it is declared by using a colon (:) after the variable name, followed by the type. For example:let x: number = 5; // x is a number let y: string = "hello"; // y is a string
By specifying the type of a variable, you can catch errors at compile time (i.e., before the code is run) that would otherwise only be caught at runtime. For example, if you try to assign a string value to a variable that has been declared as a number, you will get a compile-time error in TypeScript but not in JavaScript.let x: number = 5; // x is a number x = "hello"; // error: Type 'string' is not assignable to type 'number'
Overall, the added syntax in TypeScript for declaring variables allows for stronger type checking and can help prevent errors in your code.Functions
In addition to allowing you to specify the type of a variable when it is declared, TypeScript also provides syntax for specifying the types of function parameters and return values. This can help catch errors and make your code more readable and easier to understand.
In JavaScript, functions are declared using thefunction
keyword, and the types of the function parameters and return values are not specified. For example:function add(x, y) { return x + y; }
You can specify the types of a functionâs parameters and return value by using a colon (:) after the parameter name, followed by the type. For example:function add(x: number, y: number): number { return x + y; }
In the above example, theadd
function takes two parameters,x
andy
, which are both numbers, and it returns a value of typenumber
. If you try to call theadd
function with arguments that are not numbers, you will get a compile-time error in TypeScript but not in JavaScript.add(5, "hello"); // error: Argument of type '"hello"' is not assignable to parameter of type 'number'.
Overall, the added syntax in TypeScript for declaring functions allows for stronger type checking and can make your code more readable and maintainable. -
Basic Types
Basic Types
In TypeScript, the basic types includeboolean
,number
,string
,array
,tuple
, andenum
. These types are built into the language and can be used to specify the type of a variable or a function parameter or return value.
Theboolean
type represents a logical value, eithertrue
orfalse
. It is commonly used in conditional statements to control the flow of a program. For example:let isHappy: boolean = true; if (isHappy) { console.log("I'm feeling great!"); } else { console.log("I'm not so happy right now."); }
Thenumber
type represents a numeric value. This can be an integer, a floating-point value, or other numeric types supported by JavaScript. For example:let score: number = 100; console.log(`Your score is ${score}`);
Thestring
type represents a sequence of characters, such as a word or a phrase. Strings are enclosed in quotation marks (either single or double) and can be concatenated using the+
operator. For example:let name: string = "Alice"; let message: string = "Hello, " + name + "!"; console.log(message); // outputs "Hello, Alice!"
Thearray
type represents an ordered collection of values. The type of the values in the array must be specified using a type parameter. For example:let numbers: number[] = [1, 2, 3, 4, 5]; console.log(numbers); // outputs [1, 2, 3, 4, 5]
Thetuple
type represents a fixed-length array, where the type of each element is known but does not have to be the same. For example:let user: [string, number] = ["Alice", 25]; console.log(`${user[0]} is ${user[1]} years old.`); // outputs "Alice is 25 years old."
Theenum
type is a way to give more friendly names to sets of numeric values. For example:enum Color {Red, Green, Blue}; let favoriteColor: Color = Color.Green; console.log(`My favorite color is ${favoriteColor}.`); // outputs "My favorite color is Green."
Overall, the basic types in TypeScript provide a way to specify the type of a variable or function parameter or return value, which can help catch errors and make your code more readable and maintainable. -
Type Inference
Type Inference
TypeScript is a statically typed language, which means that the type of a variable must be specified at the time of declaring it. Type inference is a feature of TypeScript that allows the type of a variable to be automatically determined based on the value assigned to it. This is particularly useful when working with complex types, such as interfaces and classes, as it allows you to write code that is more concise and easier to read.
For example, consider the following code written in TypeScript:let x = 10;
In this code, the type of thex
variable is inferred to benumber
, based on the value assigned to it. This means that you donât have to explicitly specify the type of thex
variable, making your code more concise and easier to read.
Type inference is not limited to simple types likenumber
andstring
. It can also be used with complex types, such as interfaces and classes (we will talk more about interfaces on the upcoming chapters). For example, consider the following code:interface Point { x: number; y: number; } let p: Point = { x: 10, y: 20 };
In this code, the type of thep
variable is inferred to bePoint
, based on the value assigned to it. This means that you donât have to explicitly specify the type of thep
variable, making your code more concise and easier to read.
One of the key benefits of type inference is that it allows you to write code that is more flexible and adaptable. For example, if you need to change the type of a variable, you can simply assign a new value to it, and the type will be inferred based on the new value. This is much easier than having to manually update the type declaration every time you make a change to a variable.In summary, type inference is a powerful feature of TypeScript that allows you to write more concise and readable code. It makes it easier to work with complex types, and allows your code to be more flexible and adaptable. -
The
any
Andunknown
TypesThe
any
Andunknown
Typesany
In TypeScript, the âanyâ type is a type that allows for any value. This means that when you declare a variable with the âanyâ type, you can assign it a value of any type without getting a type error from the TypeScript compiler. For example:let myVariable: any = "hello"; myVariable = 10;
In this example, themyVariable
variable is initially declared with the âanyâ type and assigned the string value âhelloâ. Later, the value ofmyVariable
is changed to the number 10, which is allowed becausemyVariable
has the âanyâ type.The âanyâ type is often useful when migrating a codebase from JavaScript to TypeScript, as it allows you to gradually add type annotations to your code without breaking it. When migrating a large codebase, it can be difficult to add type annotations to every variable and function right away, so using the âanyâ type can help you make the transition more smoothly.However, itâs generally considered best practice to use the most specific type possible for a variable, as this can help prevent type errors and make your code easier to understand. Once you have finished migrating your code to TypeScript, you should go back and update any âanyâ type declarations to use more specific types.unknown
The âunknownâ type is a type that is more strict than the âanyâ type. While variables of the âanyâ type can be assigned values of any type without causing a type error, variables of the âunknownâ type can only be assigned values of the âunknownâ type, or values of any type if the type is checked at runtime.Type checking in TypeScript happens during compilation, not at runtime. This means that the TypeScript compiler will check the types of your variables and functions to ensure they are correct, and will give you an error if they are not. Using the âunknownâ type allows you to delay type checking until runtime, so you can ensure that the value of a variable is of the correct type before using it in your code.A real-life example of using the âunknownâ type could be when working with data that comes from an external source, such as an API call. If the structure of the data returned by the API is not known at compile time, you could declare a variable with the âunknownâ type to store the data, and then use type checking at runtime to ensure that the data has the correct structure before using it in your code.Hereâs an example of how you could use the âunknownâ type in this scenario:let data: unknown; // make an API call and store the result in the 'data' variable if (typeof data === "object" && data.hasOwnProperty("name") && data.hasOwnProperty("age")) { // the 'data' variable has the correct structure, so we can use it console.log(`Name: ${data.name}, Age: ${data.age}`); } else { // the 'data' variable does not have the correct structure, so we cannot use it console.log("Invalid data received from API"); }
In this example, thedata
variable is declared with the âunknownâ type, and the API call is assumed to return data in an object with a ânameâ and âageâ property. At runtime, the code checks the structure of thedata
variable to ensure it has the correct properties, and only uses the data if it does. This prevents type errors and helps ensure that the code is working with valid data. -
The
void
Andnever
TypesThe
void
Andnever
Typesvoid
In TypeScript, the âvoidâ type is a type that represents the absence of a value. It is commonly used for function return types to indicate that the function does not return a value.For example, here is a function that has a return type of âvoidâ:function printMessage(message: string): void { console.log(message); }
In this example, theprintMessage
function takes in a string as a parameter and logs it to the console, but it does not return a value. Therefore, its return type is âvoidâ.The âvoidâ type can also be used for variables that are never assigned a value. For example:let myVariable: void;
In this example, themyVariable
variable is declared with the âvoidâ type, but it is not assigned a value. This is allowed because the âvoidâ type represents the absence of a value, but it is generally not considered good practice to declare variables without assigning them a value.The âvoidâ type is useful for indicating that a function does not return a value, but it should not be used for variables that are intended to hold a value. Even if you will not be assigning a value when you declare it, you should type it accordingly to the value it will receive in the future.never
Thenever
type represents the type of values that never occur. This is a type that can be used in the return type of functions that always throw an exception or never return. For example:function error(message: string): never { throw new Error(message); }
In this example, theerror
function has a return type ofnever
because it always throws an exception and never returns a value.Another use for thenever
type is in the type of variables or properties that are never reassigned. For example:const a: never = error("Something went wrong");
In this example, the type of thea
variable is inferred to benever
because it is assigned the return value of theerror
function, which has a return type ofnever
.Here is another example of a function that has a return type ofnever
:function infiniteLoop(): never { while (true) { // Do something... } }
In this example, theinfiniteLoop
function has a return type ofnever
because it contains an infinite loop and never returns a value. -
Interface And Type Aliases
Interface And Type Aliases
Interface
An interface is a way to define a contract for the shape of an object. An interface specifies the names, types, and number of properties and methods that an object should have, and any object that implements the interface must have those properties and methods with the specified types.For example, if you have the following interface:interface Person { name: string; age: number; greet(): string; }
This interface defines a contract for an object that represents a person. The object must have aname
property that is a string, anage
property that is a number, and agreet()
method that returns a string. Any object that implements this interface must have these properties and methods with the specified types.To implement an interface in TypeScript, you use theimplements
keyword in the class declaration. For example, you could create aPerson
class that implements thePerson
interface like this:class Person implements Person { constructor(public name: string, public age: number) {} greet(): string { return `Hello, my name is ${this.name}`; } }
In this code, thePerson
class implements thePerson
interface by havingname
andage
properties with the same types as specified in the interface, and by having agreet()
method with the same return type as specified in the interface.Interfaces are a useful tool in TypeScript because they allow you to define a contract for the shape of an object. This can help to ensure that your code is correct and free of type errors, and it can also make your code more reusable and flexible. By using interfaces, you can define the structure of an object and create objects that conform to that structure.Type Alias
A type alias is a way to give a new name to an existing type. This can be useful if you have a complex type that you want to use multiple times in your code, but you want to give the type a more descriptive or concise name.To create a type alias in TypeScript, you use thetype
keyword followed by the new name for the type and the=
operator, followed by the existing type. For example, if you have the following type:type Person = { name: string; age: number; }
This creates a new type alias calledPerson
that is equivalent to the type{name: string; age: number;}
. This means that you can use thePerson
type alias wherever you would use the original type.Type aliases can be very simple, such as when you want to give a new name to a primitive type likestring
ornumber
. For example, you could create a type alias calledID
for thestring
type like this:type ID = string;
In this case, theID
type alias is equivalent to thestring
type, so you can use it wherever you would usestring
.Type aliases are also useful for typing callback functions. For example, if you have a function that accepts a callback as an argument, you can use a type alias to specify the type of the callback. For example:type Callback = (data: string) => void; function doSomething(cb: Callback) { // ... }
In this code, theCallback
type alias is used to define the type of thecb
argument to thedoSomething()
function. TheCallback
type is a function that takes astring
as an argument and returns nothing (void
).Overall, TypeScriptâs type alias feature allows you to give a new name to an existing type, which can make your code more readable and concise. Type aliases are useful for creating simple aliases for primitive types and for defining the types of callback functions.When to use which
Both interfaces and type aliases can be used to define the shape of an object. However, there are some differences between the two that can influence which one you should use in a given situation.Interfaces are more powerful and flexible than type aliases, because they can extend other interfaces, and be implemented by classes. This makes interfaces well-suited for defining the structure of complex objects that may have multiple implementations.On the other hand, type aliases are simpler and more concise than interfaces. They cannot be extended so easily, but this can make them easier to use and understand in some situations. Type aliases are especially useful for creating aliases for primitive types and for defining the types of callback functions.In general, you should use an interface when you want to define a complex type with optional properties, multiple implementations, or inheritance. You should use a type alias when you want to create a simple alias for an existing type or when you want to define the type of a callback function. -
Type Compatibility And Type Casting
Type Compatibility And Type Casting
Type compatibility
In TypeScript, two types are considered compatible if they have the same structure. This means that, for two types to be compatible, they must have the same properties with the same names and types.For example, if you have the following two types:interface Person { name: string; age: number; } interface Employee { name: string; age: number; salary: number; }
These two types are considered compatible because they both have the samename
andage
properties with the same types. This means that you can assign a value of typePerson
to a variable of typeEmployee
and vice versa.On the other hand, if you have the following two types:interface Person { name: string; age: number; } interface Employee { name: string; salary: number; }
These two types are not considered compatible because they do not have the same structure. In this case, theEmployee
type is missing theage
property that is present in thePerson
type. This means that you cannot assign a value of typePerson
to a variable ofEmployee
, but you can still assign a value of typeEmployee
to a variable of typePerson
because theEmployee
type has all the properties of thePerson
type.In summary, TypeScriptâs type compatibility is based on the structure of the types, and two types are considered compatible if they have the same properties with the same names and types.Type Casting
Type casting is a way to convert a value from one type to another. This is useful when you know that a value has a certain type, but TypeScript is unable to infer it.For example, if you have the following code:let someValue: any = "Hello, world!"; let strLength: number = (someValue as string).length;
In this code,someValue
is declared as typeany
, which means that it can hold a value of any type. However, you know thatsomeValue
contains a string value, so you want to use the string methodlength
to get the length of the string.To do this, you can use type casting to tell TypeScript thatsomeValue
is actually a string. This is done by using theas
keyword before thesomeValue
variable. This tells TypeScript to treatsomeValue
as a string instead of anany
type.The parentheses aroundsomeValue as string
are necessary because the.
operator has a higher precedence than theas
keyword. Without the parentheses, the code would be interpreted as(someValue as string.length)
, which is not valid. The parentheses ensure that theas
keyword is applied to thesomeValue
variable before the.
operator is applied to the result.Overall, type casting in TypeScript allows you to convert a value from one type to another when TypeScript is unable to infer the correct type. This is done using theas
keyword and parentheses to ensure that the type casting is applied to the correct value. -
Generics
Generics
What is a generic?
A generic is a type in TypeScript that allows you to define a class, interface, or function with placeholders for the types of its properties and arguments. This allows you to create a type that is flexible and can work with multiple types, rather than being tied to a specific type.Generics are useful because they allow you to write code that is more flexible and reusable. For example, you can use generics to create a function that can take arguments of any type and return a value of any type. This allows you to write code that is more generic and can be used in a wider range of situations.Generics are also useful for creating classes and interfaces that can work with multiple types. For example, you can use generics to create aQueue
class that can hold values of any type. This allows theQueue
class to be used with different types of values, such as numbers or strings, without having to create separate classes for each type.Generics are a powerful feature of TypeScript that allows you to write flexible and reusable code. By using generics, you can create classes, interfaces, and functions that can work with multiple types, which can make your code more flexible and adaptable to different situations.Working with generics
WRITE A BETTER INTROTo create an array type in TypeScript, you use theArray<T>
type whereT
is the type of the values that the array will hold. For example, to create an array of numbers, you would use the following code:let numbers: Array<number> = [1, 2, 3, 4, 5];
In this code, theArray<number>
type represents an array ofnumber
values. This means that thenumbers
variable can only hold an array of numbers, and the TypeScript compiler will check that any values that you add to the array are of typenumber
.You can also use generics to create an array type that is flexible and can hold values of multiple types. For example, you can use theArray<T>
type with a union type to create an array that can hold multiple types of values. For example:let values: Array<string | number> = ["hello", 42, "world"];
In this code, theArray<string | number>
type represents an array that can hold eitherstring
ornumber
values. This means that thevalues
variable can hold an array of eitherstring
ornumber
values, or a mixture of both types.TypeScriptâs generics feature allows you to create array types that are specific to a certain type of value. By using generics, you can create arrays that can hold values of a specific type, and the TypeScript compiler will check that the values in the array are of the correct type at runtime. This can help to ensure that your code is correct and free of type errors. -
Async Stuff
Async Stuff
Promise
Promises are a useful tool for working with asynchronous code in JavaScript because they provide a way to handle the results of asynchronous operations consistently and predictably. However, promises can be somewhat complex to work with, because they require you to use callback functions and manage the state of the promise.Theasync
andawait
keywords in TypeScript provide a way to simplify working with promises by allowing you to write asynchronous code that looks and behaves like synchronous code. When you useasync
andawait
, you can use theawait
keyword to wait for the result of a promise, and the code that follows theawait
keyword will be executed only when the promise is resolved. This makes it easier to write asynchronous code that is easy to read and understand.However, it is important to note thatasync
andawait
are just syntax sugar on top of promises. They are not a completely different way of writing asynchronous code, but rather a way to make promises easier to work with. Behind the scenes,async
andawait
still use promises to manage the flow of asynchronous operations.Typing promises
You can use the built-inPromise
type to specify the type of a promise. ThePromise
type is a generic type that takes two type arguments: the type of the resolved value of the promise, and the type of the rejected value of the promise.For example, if you have a promise that resolves to astring
and rejects with anError
object, you could specify the type of the promise like this:let promise: Promise<string, Error>;
In this code, thePromise<string, Error>
type specifies that thepromise
variable is a promise that resolves to astring
and rejects with anError
object.You can also use thePromise
type to specify the type of a function that returns a promise. For example, if you have a function that returns a promise that resolves to astring
and rejects with anError
object, you could specify the type of the function like this:function someAsyncFunction(): Promise<string, Error> { // asynchronous code goes here }
In this code, thePromise<string, Error>
type specifies that thesomeAsyncFunction()
function returns a promise that resolves to astring
and rejects it with anError
object.ThePromise
type in TypeScript is a useful tool for specifying the type of a promise, and it allows you to specify the type of the resolved and rejected values of the promise. This can help to ensure that your code is correct and free of type errors.
Using It Effectively
Using TypeScript effectively means taking advantage of its
features to write clear and reliable code that is easy to
maintain and modify. This involves using TypeScriptâs strong
typing system to catch potential errors before they happen,
using interfaces to define the shape of complex data structures,
and using classes and modules to organize your code in a logical
and reusable way. We will also cover some common errors you might
see coming from TypeScript so you can understand what is going
on and reach a solution faster. Overall, the effective use of
TypeScript can help you write code that is safer, more scalable,
and easier to work with.
-
Working With Classes
Working With Classes
If you are used to working with object-oriented programming, chances are you are already familiar with classes and you can skip this section. But if you are not or need a little reminder, continue reading below.What is a class?
A class is a blueprint for creating objects that share the same properties and methods. It defines the structure of an object, including its data fields (attributes) and the functions that operate on those fields (methods).
It is a way of organizing and structuring code, and it provides a way to create objects that have a common structure and behavior. Classes can be used to model real-world entities, such as cars, animals, or people, and they can be used to create abstract concepts, such as a vector or a linked list.Defining a class
You can create a class using theclass
keyword, followed by the name of the class. Here is an example of a simplePerson
class:class Person { // class body }
The class body is where we define the attributes and methods of the class. For example, we could define aname
attribute and agreet()
method like this:class Person { name: string; greet() { console.log(`Hello, my name is ${this.name}`); } }
Thename
attribute is a string, and thegreet()
method prints a greeting message to the console. Note that we use thethis
keyword to refer to the current objectâs attributes and methods.The constructor
Theconstructor
function is a special function that is called when an object is created from a class. It is used to initialize the objectâs data fields and run any bootstrapping logic. Here is an example of aPerson
class with aconstructor
function:class Person { name: string; constructor(name: string) { this.name = name; } greet() { console.log(`Hello, my name is ${this.name}`); } }
Theconstructor
function takes aname
argument and assigns it to thename
attribute of the object. This ensures that eachPerson
object has aname
attribute that is set to the correct value.Creating objects
Once we have defined a class, we can create objects from that class using thenew
keyword. Here is an example of creating aPerson
object and calling itsgreet()
method:let person = new Person("John"); person.greet(); // prints "Hello, my name is John"
In this example, we create aPerson
object and assign it to theperson
variable. We pass the"John"
string as an argument to theconstructor
function, which sets thename
attribute of the object. Then we call the objectâsgreet()
method, which prints a greeting message to the console.Inheritance
Inheritance is a fundamental concept in object-oriented programming (OOP) that allows one class to inherit the attributes and methods of another class. In TypeScript, inheritance is implemented using theextends
keyword.The idea behind inheritance is to create a hierarchy of classes, where a subclass can inherit the attributes and methods of its superclass. This allows us to reuse code and avoid duplication. For example, we could define aVehicle
class that has adrive()
method, and then create subclasses for specific types of vehicles, such asCar
andTruck
, which inherit thedrive()
method from theVehicle
class.
Here is an example of how we could define aVehicle
class and aCar
subclass that inherits from it:class Vehicle { drive() { console.log("Driving..."); } } class Car extends Vehicle { // additional car-specific methods and attributes }
In this example, theCar
class inherits thedrive()
method from theVehicle
class. This means that we can create aCar
object and call itsdrive()
method, and it will print âDrivingâŚâ to the console.Inheritance allows us to create subclasses that are specialized versions of their superclass. For example, theCar
class could have additional methods and attributes that are specific to cars, such as ahonk()
method or anumDoors
attribute. These attributes and methods would be in addition to the ones that it inherits from theVehicle
class.
You can use thesuper
keyword to call the constructor of the superclass from within a subclass. This is often used to pass arguments to the superclassâs constructor and initialize the objectâs data fields. Here is an example of using thesuper
keyword to call theVehicle
classâs constructor from within theCar
class:class Vehicle { numWheels: number; constructor(numWheels: number) { this.numWheels = numWheels; } drive() { console.log("Driving..."); } } class Car extends Vehicle { numDoors: number; constructor(numDoors: number) { super(4); // call the Vehicle class's constructor this.numDoors = numDoors; } // additional car-specific methods and attributes }
In this example, theCar
classâs constructor calls theVehicle
classâs constructor using thesuper
keyword. This passes the4
argument to theVehicle
classâs constructor, which sets thenumWheels
attribute of the object to4
. TheCar
classâs constructor then sets thenumDoors
attribute of the object.Encapsulation
Encapsulation refers to the mechanism of restricting access to the internal properties and methods of a class from outside it. To achieve that you will need to use accessibility modifiers. In TypeScript, the accessibility modifiers arepublic
,private
, andprotected
. These keywords determine whether a class member can be accessed from outside the class or from within derived classes (subclasses).public
Class members that are marked aspublic
can be accessed from anywhere, both inside and outside the class where they are defined. Class members that do not have an explicit accessibility modifier are consideredpublic
by default.Here is an example of aPerson
class with apublic
attribute:class Person { public name: string; constructor(name: string) { this.name = name; } }
In this example, thename
attribute ispublic
, so it can be accessed from anywhere. We can create aPerson
object and access itsname
attribute like this:let person = new Person("John"); console.log(person.name); // prints "John"
It is a good practice to make it public only what should actually be acessible externally. For internal behaviour of your objects you should use one of the next two accessibility modifiers we are going to see here.private
Theprivate
modifier restricts the accessibility of a class member to within the class in which it is defined. This means that private properties or methods cannot be accessed from outside the class or from derived classes.Hereâs an example:class Person { private name: string; constructor(name: string) { this.name = name; } public sayHello() { console.log(`Hello, my name is ${this.name}.`); } } const john = new Person('John'); john.sayHello(); // Output: "Hello, my name is John." console.log(john.name); // Compilation error: Property 'name' is private and only accessible within class 'Person'.
In this example, thePerson
class has a privatename
property which can only be accessed within thePerson
class. The constructor of thePerson
class initializes the name property with the value passed to it.ThesayHello
method is declared as a public method, which means it can be accessed from outside the class. ThesayHello
method uses the name property to log a message to the console. Remember that methods and properties are public by default, so thepublic
keyword can be ommited.When we create a newPerson
instance and call thesayHello
method, it logs the message to the console with the value of the name property. However, if we try to access thename
property directly from outside the class (as shown in the console.log statement), we will get a compilation error, because thename
property is private and can only be accessed within thePerson
class.protected
Theprotected
modifier is similar to private, in that it restricts the accessibility of a class member. However, protected members can be accessed within the class in which they are defined and within any classes that inherit from that class. This means that protected members can be accessed by the class itself and by any derived classes, but not by code outside of the class hierarchy.Hereâs an example:class Person { protected name: string; constructor(name: string) { this.name = name; } protected sayName() { console.log(`My name is ${this.name}.`); } } class Employee extends Person { private id: number; constructor(name: string, id: number) { super(name); this.id = id; } public introduce() { this.sayName(); console.log(`I am an employee with ID ${this.id}.`); } } const john = new Employee('John', 123); john.introduce(); // Output: "My name is John." followed by "I am an employee with ID 123." console.log(john.name); // Compilation error: Property 'name' is protected and only accessible within class 'Person' and its subclasses. john.sayName(); // Compilation error: Property 'sayName' is protected and only accessible within class 'Person' and its subclasses.
In this example, we have aPerson
class with a protectedname
property and a protectedsayName
method. The Employee class extendsPerson
and adds a privateid
property and a publicintroduce
method that calls the sayName method from thePerson
class.We can create anEmployee
instance and call itsintroduce
method, which calls thesayName
method from thePerson
class. These methods are accessible from theEmployee
class because it inherits fromPerson
.However, if we try to access thename
property or call thesayName
method directly from theEmployee
instance, we will get a compilation error, because these members are protected and can only be accessed within thePerson
class and its subclasses.Abstract classes and methods
An abstract class is a class that cannot be instantiated directly, but can only be used as a base class for other classes. An abstract class can contain both abstract and non-abstract methods and properties.An abstract method is a method that is declared but does not provide an implementation. Abstract methods are intended to be implemented by the derived classes, which inherit from the abstract class. Abstract methods are declared using theabstract
keyword, and must be implemented in the derived class using theoverride
keyword. Theoverride
keyword might not be neccesary, depending on your typescript configuration.Abstract classes are often used to define a common interface or behavior that can be shared by multiple derived classes. By defining an abstract class, developers can ensure that the derived classes adhere to a common contract, while still allowing the derived classes to implement their own specific behavior.Here is an example of an abstract class in TypeScript:abstract class Person { protected name: string; protected age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } abstract sayHello(): void; walk(distanceInMeters: number = 0) { console.log(`${this.name} walked ${distanceInMeters}m.`); } } class Employee extends Person { private id: number; constructor(name: string, age: number, id: number) { super(name, age); this.id = id; } override sayHello() { console.log(`Hello, my name is ${this.name} and my employee ID is ${this.id}.`); } }
In this example,Person
is an abstract class that defines aname
property, anage
property, awalk
method, and an abstractsayHello
method.Employee
is a concrete class that extendsPerson
and adds a newid
property.Employee
overrides thesayHello
method inherited fromPerson
with its own implementation.Hereâs how you could use theEmployee
class:const employee = new Employee("John Doe", 30, 12345); employee.sayHello(); // Output: "Hello, my name is John Doe and my employee ID is 12345." employee.walk(100); // Output: "John Doe walked 100m."
This is not all you can do with classes, but what was explained so far should be enough to follow what is to come. -
Using Abstractions
Abstractions
Abstraction is a fundamental concept in computer programming that refers to the process of simplifying complex systems or ideas by focusing on the essential details and ignoring unnecessary details. In other words, you use abstraction when creating a model or representation of a system that exposes what you need to know and abstracts away implementation detail that is not important to every consumer of that specific piece of code.Abstraction is achieved by defining abstract classes or interfaces that specify a set of methods and properties that a concrete class must implement. This allows the concrete class to implement its own specific behavior while still adhering to the contract specified by the abstract class or interface. In other words, you have abstractions that describes something and imposes rules but doesnât really implements anything. And you have the concrete implementation, that might stand on itâs own or be bound by what what an abstraction has defined.Abstraction is used to simplify programming tasks and reduce the complexity of code. It makes it easier to reason about a system, as developers can focus on the high-level design rather than the low-level implementation details. Abstraction is a key concept in Object-Oriented Programming (OOP), and is often used in conjunction with encapsulation and inheritance to create robust and maintainable code.Interfaces
Here are a few examples where interfaces can be used to create an abstraction:UI components: UI libraries often define interfaces for components, such as buttons or menus, that are used to define a common set of properties and methods that are shared by all instances of that component.Database access: Interfaces can be used to define a common API for accessing different types of databases, such as relational or NoSQL databases, making it easier to switch between them.Networking: Interfaces can be used to define a common set of methods and properties for interacting with different network protocols, such as HTTP or WebSockets.Letâs take the example of UI components and expand it to show some example code:interface UIComponent { id: string; isVisible: boolean; render(): void; destroy(): void; }
In this example,UIComponent
is an interface that defines a common set of properties and methods for UI components. It has anid
property, anisVisible
property, and two methods,render
anddestroy
.Hereâs an example of aButton
class that implements theUIComponent
interface:class Button implements UIComponent { id: string; isVisible: boolean; label: string; constructor(id: string, isVisible: boolean, label: string) { this.id = id; this.isVisible = isVisible; this.label = label; } render() { // Render the button HTML } destroy() { // Remove the button from the UI } }
By defining theUIComponent
interface, we can ensure that all UI components in our application have a common set of properties and methods, making it easier to work with and maintain our UI code. TheButton
class can be used alongside other UI components that also implement theUIComponent
interface.Abstract classes
Sometimes you need something more than what interfaces can offer, this is when the abstract classes that we talked about earlier come in handy.Interfaces cannot provide any implementation details. They simply define the structure of the contract that a class must adhere to. Interfaces are useful when you want to define a contract that multiple classes can implement, regardless of their inheritance hierarchy. Abstract classes can provide implementation details in addition to the contract they define. This means that you can provide some common concrete implementation for all the classes that inherit from the abstract class.One example of how abstract classes can be used is how the framework NestJSprovides the abstraction for HTTP adapters. NestJS is a framework that can, among other things, expose HTTP APIs using different HTTP server implementations, such as express or fastify. To accomplish that an AbstractHttpAdapter
is provided and extended by the express and fastify HTTP adapters.Here is a simplified version ofAbstractHttpAdapter
:export abstract class AbstractHttpAdapter { protected httpServer: TServer; public getHttpServer(): TServer { return this.httpServer as TServer; } public setHttpServer(httpServer: TServer) { this.httpServer = httpServer; } abstract initHttpServer(options: NestApplicationOptions); }
In this example you can see that it provides agetHttpServer
and asetHttpServer
method, and both are concrete methods. Meaning they have an implementation that should be the same for all sub-classes. But it also has an abstractinitHttpServer
that needs to be overriden by the sub-classes making it specific for that particular adapter.I tried to bring a more practical example this time as we will start using the theory we have learned so far to implement things more useful than a boring genericPerson
class that has aname
and anage
. Hopefully a more practical approach will also help fixate any concepts that might not be very clear for you so far and make you more confident in applying these to your projects. -
Modules
Modules
This is not specific to TypeScript, but still I would like to talk about it. The way you organize your code and impose boundaries between different parts of your project is very important to how effectively you can make use of it. TypeScript is going to be an important tool to shape those boundaries creating contracts between different parts of your code.Splitting your code by domains
A domain represents a specific area of functionality within an application, such as user management, payment processing, or product inventory. By organizing your code around domains, you can reason about it better.Navigating around a project organized like that is easier because its folder structure is expressive and you know where to put your code or where to find what you need to modify.Having acore
orfeatures
ormodules
folder (or any other name you wanna give it) with a folder for each domain, makes it much easier to know where the login or payment feature is implemented.If you have experience as a professional software developer you most likely have seen a project that had these three folders at the top level: âmodelsâ, âviewsâ and âcontrollersâ. This was very popular for a while, but in fact, itâs not a great way to organize a project, often causing it to rotten quite quickly.MVC is a great design pattern, but using it to organize all of your code as a top-level architecture is misusing it. It fails to make your folder structure expressive of what its domains are and it spreads domains along those three different folders instead. MVC was created for dealing with GUI and not for organizing a whole application. That is why working on a big MVC application feels like hammering screws on a plank.Creating boundaries to external code
One time I went to check on one of those bugs that happen every now and then, but no clear reproduction scenario was available. I debugged it for a bit and checked which libraries it was using, finding a real sus one. Unmaintained for 4 years and full of open issues open on GitHub. I did a quick search on the issue and more people were relating the same thing.It was clear to me that I needed to refactor the project to remove that library. I was lucky it was a small project with not a lot of lines of code, but I was unlucky in that it was a library that was widely used in that project. It affected every feature and had crept its way into almost every file.Most libraries should not be used directly, as it will make your project depends on them. Sometimes you might need to swap them for a different one. Either because they have become insufficient technically or because of a business decision in the case of paid external services and libraries.It is much better to invert the dependency when possible and define how your code expects to, for example, send a push notification to a customer independently from what library is being used to do the chore.A very basic implementation of that would be to simply define an interface such as:interface PushManager { send(customerID: string, message: string): void; }
And then create classes that map it to the actual implementation for a specific service.// serviceA.ts import { send } from 'serviceA'; export class PushServiceA implements PushManager { send(customerID: string, message: string) { send(customerID, message); } } // serviceB.ts import ServiceB from 'serviceB'; export class PushServiceB implements PushManager { service = new ServiceB(); send(customerID: string, message: string) { this.service.sendPush({ identifier: customerID, body: message }); } }
You can see thatserviceA
andserviceB
have different API and way of working to accomplish the same goal. But with this implementation you can swap between them quite easily since your code only needs to know the signature of thePushManager
interface and doesnât need to care about how it is implemented.I would recommend segregating the code that deals with libraries and external services from your core code where your business logic is implemented. Not all frameworks and libraries reward this practice, so donât go crazy about it as well. Perfect is the enemy of good. -
Reading TypeScript Errors
Reading TypeScript Errors
It, sometimes, can be a little hard to understand Typescriptâs errors. They can be a bit verbose and not that straight forward, but after you get used to them, the errors become less confusing and easier to read.I often see developers that ignore the error message and just try to figure out what is wrong by re-reading the code. It might be a bit hard to follow the error messaged at first, but you should still try to understand what they are saying, even if that means taking them apart and reading sentence by sentence trying to identify what how it maps to what you are doing on your code. The more you do it the better you get and the more useful the error messages will become to you. I hope this short description of some of the ones I struggled a bit with will be helpful to you as well.Type âxâ is not assignable to type âyâ
This is an issue with type compatibility. You can follow the link for a better explanation, but check your code and make sure the variable you are assigning or parameter you are passing is compatible with what is expected. They can be of different types, but those types need to have a signature that matches with each others requirements.Property âxâ does not exist on type âyâ
This error occurs when you try to access a property that does no exist on a given object type. This can happen when you misspell a property name or when the object doesnât have that property. Often you have the wrong object or you are not deep enough on it, such as when parsing an API response where you though you couldresponse.someProperty
when you actually need toresponse.data.someProperty
.Argument of type âxâ is not assignable to parameter of type âyâ
This one occurs when you pass an argument of the wrong type to a function. This can happen when you pass a string to a function that expects a number or pass an object that doesnât match the expected interface.Cannot redeclare block-scoped variable âxâ
Kinda straight-frward really, you are trying to declare a variable that has already been declared. This can happen when you declare a variable twice within the same function or module.Module ââxââ has no exported member âyâ
This happens when you try to import something out of a module and itâs not there. This can happen when you misspell what you are trying to import or when you forgot to add theexport
keyword to make it accessible.Property âxâ is missing in type âyâ
This error occurs when you try to assign an object that does not match the expected interface. This can happen when you forget to include a required property or when you include an extra property that doesnât exist in the interface.Function lacks ending return statement and return type does not include âundefinedâ
When a function does not have a return statement or when the return type does not include âundefinedâ. This can happen when you forget to add a return statement or when the function should return a value but doesnât. Check both that you do have a return statement and that it matches the return type you declared for the function. Sometimes you have anif
that means your function is not returning a value in every possible outcome.