Learning TypeScript Fundamentals from Scratch (Part 2 — Syntax and Features)

Andrew Davis Escalona
7 min readDec 2, 2018

--

This article is the second part of my Learning TypeScript Fundamentals series. If you haven’t seen the first part, you can visit it here:

On this second article, we are going to learn more about the basic syntax, and how to use the essential features of TypeScript. To achieve this, I’m assuming you have some experience working with vanilla JavaScript — whether it’s node or web — or at least you know its basis.

Keep in mind that TypeScript is a JavaScript superset. Thus, any JS feature or syntax is still available on TS as well. Nevertheless, it’s always recommended to use the TS features anytime you have the chance.

Using types when declaring variables or constants

Declaring variables is quite simple with TypeScript. We need to write the data type after writing the variable name and a colon:

let myBoolean: boolean = true;
let myNumber: number = 2;
let myString: string = "hi";

We can also set a variable to have multiple data types by using a pipe between them:

let myStringOrNumber: number | string = "hi";
myStringOrNumber = 1;

Or to define any data type possible by using “any”:

let thisCouldBeAnything: any = 1;
thisCouldBeAnything = true;
thisCouldBeAnything = "anything!";

Although using “any” might be handy at times, its usage is discouraged, since the whole point of using TypeScript is to have strict types.

Declaring Arrays

Declaring arrays is also quite straightforward. You only have to include a couple of empty brackets after the data type:

let myBooleanArray: boolean[] = [true, false];
let myNumberArray: number[] = [1, 2, 3];
let myStringArray: string[] = ["hello", "world"];

Declaring Tuples

Tuples might be a new concept if you are not familiar with languages like Python. They are just a way to represent sets of data with a fixed structure. For example:

let myCoordinatesTuple: [string, number, number] = ["Eiffel Tower",  48.858271, 2.294524];

In this example, we are declaring a tuple for a landmark with its latitude and longitude coordinates (Eiffel Tower).

Declaring Enums

You can use enums to represent sets of constants. For example:

enum response { No, Yes }

Enums are index based. So if you reference the above enum declaration with “0” and “1”, you will get “No” and “Yes” respectively.

console.log(response[0]); // No
console.log(response[1]); // Yes

You can also set your custom enum values:

enum customResponse {
NO = "No",
YES = "Yes"
}

And set your index values too:

enum positionWithIndex {
up = 1,
down,
left,
right
}

If we only set the first key with a number, the following values are going to increase in one:

console.log(positionWithIndex.up); // 1
console.log(positionWithIndex.down); // 2
console.log(positionWithIndex.left); // 3
console.log(positionWithIndex.right); // 4

Objects and Interfaces

Using simple objects in TS is not as straightforward as it is in vanilla JS. Check this example:

let myStudent: object = { name: "Andrew", age: 30 };
myObject.age = "32";

If we try to compile the last chunk of code, we will get the following error:

main.ts:71:10 - error TS2339: Property 'age' does not exist on type 'object'.

Such error is happening because, to use objects correctly, we need to define an interface first. Otherwise, it is not possible to modify any of its property values. Just in case you don’t know, an interface is pretty much a contract defined for a class or function.

In our case, we should define an interface like this:

interface IStudent {
name: string;
age: number;
}

So that we can create a correct student object as follows:

let student: IStudent = { name: "Andrew", age: 30 };
student.age = 32; // Now we can modify this value

Functions

To correctly declare a function in TS, we need to define its parameters and return types as follows:

const mySum = (val1: number, val2: number): number => {
return val1 + val2;
};

If the function doesn’t return any value, you must set void as the return type:

const sayHello = (): void => {
console.log("hello!");
};

Classes and Inheritance

If we wanted to declare a class with TS, we would do something like this:

class Person {
firstName: string;
lastName: string;
age: number = 0;

constructor(data?: any) {
this.firstName = data.firstName || null;
this.lastName = data.lastName || null;
this.age = data.age || this.age;
}
}

The last example looks similar to the way we define classes with vanilla JS, but there are some differences. First, notice how we can set class properties outside its constructor, and we can also initialize those properties as well as to define their data types. Additionally, see the quotation mark within the constructor parameters, which indicates that such parameter is optional.

Let’s now suppose we want to create a child class. We should do something like this:

class Student extends Person {
course: string;
constructor(data: any, course: string) {
super(data);
this.course = course;
}
}

Nothing new here, we still have to use the “super” keyword to instantiate the parent class.

Access Modifiers

In TypeScript, we have the option to use access modifiers to have more control when it comes to accessing the class properties.

Initially, we count with the following list of modifiers:

  • public: indicates you can access the property from anywhere. It’s the default behavior, so it’s not required to define it explicitly.
  • private: means you can only access the property from inside the class.
  • protected: indicates you can access the property either from inside the class or any child class.

Here, there is an example:

class Building {
public address: string;
private costructionYear: string;
protected size: number;
public readonly architect: string = "Andrew";
public readonly owner: string;

constructor(
address: string,
costructionYear: string,
size: number,
owner: string
) {
this.address = address;
this.costructionYear = costructionYear;
this.size = size;
this.owner = owner; // we cannot modify it anymore!
}
}
// Child classclass House extends Building {
constructor(
address: string,
costructionYear: string,
size: number,
owner: string
) {
super(address, costructionYear, size, owner);
}
printSize(): void {
console.log(this.size); // this is a parent property
}
}

We can get the “size” property from the “House” class because it was declared as protected on its parent class. Additionally, readonly means we cannot modify a property once is initialized.

Generics

With Generics we can define data types and classes as parameters, which is useful when we don’t have a clear idea about what data type or instance a function or class might use.

Let’s suppose we have two types of pets with different actions. For example:

class Cat {
meow() {
console.log("meowwwwww");
}
}
class Dog {
bark() {
console.log("wufff");
}
}

This is just an example! Probably there are better ways to implement this, but the main idea is to have two classes with different implementations.

Now, if we liked to have a third class — let’s call it “Owner” — that can accept both of our classes (“Cat” and “Dog”), we would do something like this:

class Owner<T> {
public name: string;
public pet: T;
constructor(name: string, pet: T) {
this.name = name;
this.pet = pet;
}
}

Notice the <T> syntax, which means this class can accept a data type or class as a parameter. So if we wanted to create an instance of this class, we should do something like this:

const owner1: Owner<Cat> = new Owner<Cat>("Andrew", new Cat());
const owner2: Owner<Dog> = new Owner<Dog>("Margareth", new Dog());
owner1.pet.meow();
owner2.pet.bark();

Here we’re creating two instances of “Owner,” one that only accepts “Cat” as the pet property, and the other one that only accepts “Dog.”

Similarly, we can do the same with interfaces. Let’s suppose a scenario when we have to define two types of residence profile for two countries. For example, the Venezuelan national ID consists of only numbers, but in Chile, a letter “K” might be allowed. So a string would be more convenient. An implementation for this scenario would look like this:

interface IResidence<T> {
name: string;
id: T;
}
const residentFromVenezuela: IResidence<number> = { name: "Andrew", id: 123456 };const residentFromChile: IResidence<string> = { name: "Andrew", id: "12345-K" };

Now we can use the same interface for two different country residences. One allows a numeric id, and the other one allows a string.

And for the last, this is an example of how to use interfaces with functions:

const loggingIdentity = <T>(arg: T[]): T[] => {
console.log(arg.length);
return arg;
}
const someNumbers: number[] = loggingIdentity<number>([1, 2, 3, 4]);
const somewords: string[] = loggingIdentity<string>(["hello", "world"]);

So in this case, we might have a function accepting arrays of strings, numbers or any other data type or class. In the end, the only thing we would care is to print the array length.

Conclusion

So this is it! I hope I have covered the essential parts of TypeScript programming. Obviously, there are more advanced features like decorators, meta-programming and other stuff. But I’ll let you investigate those for yourself (Google is your friend).

If you want to take a look at the code used in this article. You can visit the GitHub repo here:

--

--