Typescript Fundamentals

M. K. Bughowi

July 2022, 30

11 min read

Typescript Fundamentals

So originally I was resistant to even learn TypeScript because I wasn’t comfortable with strong typed languages and I really tried to avoid writing any more code that I have to. But this is the situation where writing a little more code upfront will pay big devidends as your project grows.

Benefit

The biggest benefit is actually just tooling that you get in your IDE like vs code. When you use type annotation or work with strong typed library, your code will be documented in the IDE so you really have to refer back to online documentation for the libraries that you use. In addition the compiler that can catch bugs in advance which is a far more efficient way to refactor code.

Another cool benefit of typescript is that there’s virtually no learning curve if you know JavaScript. That’s because it’s a superset of JavaScript. So any valid JS code is also valid in TypeScript. You can learn it incrementally as you go. And it also allows us to write code with future JavaScript features without having to worry about whether or not this code will be supported in our environment because we can transpile it to multiple JavaScript flavors.

Install TypeScript

Now that you know TypeScript is awesome, let’s go ahead and started. The first you want to do is install TypeScript globally with NPM. Doing this will give you access to the tsc command which will run the typescript compiler.

Terminal window
npm i -g typescript

Compile TypeScript to JavaScript

So the first we’ll do is create an index.ts file and TypeScript on it’s own can’t run anywhere. It won’t work in the browser or Node.js or anything like that. What we do is use the TypeScript compiler to convert TypeScript code to vanilla JavaScript.

Let’s start by writing some plain JavaScript in our TypeScript file and then compile it.

index.ts
console.log('hello world');

Go down to command line and run tsc index.ts.

You’ll notice it creates an index.js file that’s our actual JavaScript code which we can run in the browser or node. And because we just wrote plain JavaScript, the code is identical to what’s in the index.ts file.

index.js
console.log('hello world');

By default, TypeScript will compile to ES3 which dosn’t support for async await. So let’s see what’s happen when we write an async function in our .ts file and the compile it.

index.ts
async function hello() {
return 'world';
}

You’ll notice here that our code gets transpiled to this creazy looking JavaScript so we can use async await in our main TypeScript code.

index.js
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator['throw'](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
// +33 lines

Compiler

The compiler is basically very sophisticated. There’s a ton of different options that you can pass to customize it’s behavior. You could pass the options from the command line but the standart to do it is to create a tsconfig.json which will automatically get picked up when you run tsc command.

The tsconfig.json can be be pretty overwhelming at first but there is usually only this options that you have to think about for the most part.

target

The first one target and this is the flavor of JavaScript that your code will compiled to. So if we set target to esnext and then run tsc, you’ll see that it compiles our code with async await natively. It’s targeting the latest verison of JavaScript which supports that syntax.

tsconfig.json
{
"compilerOptions": {
"target": "esnext"
}
}

watch

Another option that we want to set right away is "watch": true which will just recompile our code every time we save the file. It will just save us from rerunning the tsc command after every change.

tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"watch": true
}
}

lib

The next option we will look at is lib which allows us to automatically include typings for certain environtments such as the DOM or ES 2017. So if you’re building a web application you’d want to include that DOM library which allows TypeScript to compile your code with all the native DOM classes without any compilation error.

tsconfig.json
{
"compilerOptions": {
"target": "es3",
"watch": true,
"lib": ["dom", "es2017"]
}
}

For example if we go back to our code, we can use URL class which is part of the DOM and we’ll got autocomplete and intellisense on this class. So this is where the incredible tooling of TypeScript starts to come in. If we hover over the class we have intergrated documentation as well as an error message telling us exactly why this code won’t run.

autocomplete and intellisense in typescript

Type

Now we know how the TypeScript compiler works, let’s go ahead and write some code that uses type annotations. There are two ways you can strong type your code implicitly or explicitly.

Implicit Type

Let’s say we have a variable that should be a number. If we assign the value to this variable when it’s declared. it’s type will automatically be inferred.

index.ts
let lucky = 23; // lucky will have number type

Then if we try to assign a string value to this variable it’s going to give us an error because a string is not assignable to number. If this code were vanilla JavaScript we wouldn’t catch this bug until we actually run this code somewhere. But with TypeScript we know about it right away.

index.ts
let lucky = 23; // lucky will have number type
lucky = '23'; // error because we can't assign string value to number variable

Unlike languages like C# or Java, we can actually opt out the type system annotating our variable with any.

index.ts
let lucky: any = 23; // lucky will accept all types of value
lucky = '23'; // including string

This just means that this variable can be assigned any value and the compiler won’t type check it. Ideally you want to avoid doing things like this when possible but it does give TypeScript a ton of flexibilty.

Explicit Type

In above example we gave our variable an impicit number type, but what if we don’t have a value to assigned to it upfront. If we don’t add any type annotations, it’s going to be infered as an any type, so we can assign both a string and number to it. If we want to annotate it with a type, we can just write colon followed by number which is one of the built-in primitive types in JavaScript. When we do that, we get an error under the string value because we can’t assign it as that type.

index.ts
let lucky: number; // lucky type will be number
lucky = '23'; // error assign string value
lucky = 23; // no error

Define Custom Types

So we’ve looked at some of the built-in types in JavaScript. But you can also create your own types. First you’ll give the type a name which is typically in Pascal case.

type Style = string;

Then we can declare a variable that’s annotated with this Style type and then we’ll get feedback for this custom type instead of just regular string.

type Style = string;
let font: Style;

Let’s say our style type can be only be bold or italic. We can create a union type by separating theme with a pipe. And now we can only assign this variable to this two specific values. And we’re not limited to just string, we could even extend this custom type with a number.

type Style = 'bold' | 'italic' | 23;
let font: Style;

Interface

So that’s pretty cool but more often, you’ll be strong typing object that have multiple property with different multiple types. Let’s imagine we have two objects and we want to enforce that this object shape has a first and last name with string types.

const person1 = {
first: 'Harry',
last: 'Maguire',
};
const person2 = {
first: 'Usain',
last: 'Bolt',
fast: true,
};

Composing objects or class instances that don’t have the correct shape is an easy way to create bugs. But with TypeScript we can enforce the shape of an object with an interface. If we know the shape of object would be the same, then we can define an interface that defines the type of each property.

interface Person {
first: string;
last: string;
}

Now we can use this interface to strong type this object directly or we could use it as the return value from a function, argument, or anywhere else in our code.

interface Person {
first: string;
last: string;
}
const person1: Person = {
first: 'Harry',
last: 'Maguire',
};
const person2 = {
first: 'Usain',
last: 'Bolt',
fast: true,
};

Now sometimes an interface like this can be too restrictive. You can maintain the required properties and then add additional properties by creating a key with a type of string with a value type of any. So now a first and last name will be required, but you also can add any additional property that you want to this object.

interface Person {
first: string;
last: string;
[key: string]: any;
}
const person1: Person = {
first: 'Harry',
last: 'Maguire',
};
const person2 = {
first: 'Usain',
last: 'Bolt',
fast: true,
};

Function

Now let’s go ahead and switch to function. Strong typing in a function can be a little more complex because you have types for arguments and also the return value. Here we just have plain JavaScript function without any types.

function pow(x, y) {
return Math.pow(x, y);
}

Then we could add string values as the arguments and we wouldn’t any error from the compiler. Bu obviously this function is going to fail if we try to pass it any non-number value.

function pow(x, y) {
return Math.pow(x, y);
}
pow('23', 'foo');

You can annotate arguments the same way we do with variables. That will sure only numbers can be passed to this function.

function pow(x: number, y: number) {
return Math.pow(x, y);
}
pow(5, 10);

So the function implicitly has a number return value because we’re using the native math JavaScript library, but we can annotate a specific return value type after the parentheses and before the bracket. So if we set that type to a string you’ll see it’s underlined in red because it’s returning a number. To implement this function correctly we can call toString() method.

function pow(x: number, y: number): string {
return Math.pow(x, y).toString();
}
pow(5, 10);

In many cases you might have functions that don’t return a value or create some kind of side effect, in that case you can type your function return value to void.

The next thing we’ll look at is how to strong type an array so we’ll start by creating an empty array then pushing a different values to it with different types.

const arr = [];
arr.push(1);
arr.push('23');
arr.push(false);

We can force this array to only have number types by doing a number type followed by bracket, signifying that it’s an array. Now you can see we get an error every time to push a value that’s not a number.

const arr: number[] = [];
arr.push(1);
arr.push('23'); // error
arr.push(false); // error

This is especially useful when you’re working with an array of objects and you want to get some intellisense as you’re iterating over those objects.

TypeScript also opens the door to a new data structure called a tuple. Basically this is just a fixed length array where each item in that array has it’s own type.

type MyList = [number, string, boolean];
const arr: MyList = [];
arr.push(1);
arr.push('23');
arr.push(false);

You can each items values optional by putting a question mark after the type. You also can use this question mark syntax in other places.

type MyList = [number?, string?, boolean];

Generic

The last thing is TypeScript generics. You may run into situation where you want to use type internally inside of a class or function. For example here I have Observable class that has an internal value that you can observe.

class Observable<T> {
constructor(public value) {}
}

The T represents a variable type that we can pass in to strong type this Observable internal value. This allows us to specify the internal type at some point in our code. For example we have an Observable of a number or person interface likes below. Or we can also do this implicitly if we create a new Observable of a number, it’s going to implicitly have that internal number type.

class Observable<T> {
constructor(public value: T) {}
}
let x: Observable<number>;
let y: Observable<Person>;
let a = new Observable(23);

More often than not, you’ll be using generics rather than creating them. But it’s definitely important thing to know.

I’m gonna go ahead and wrap thing up there. Hopefully this article give you an idea of why TypeScript is so powerful but we really only scratched the surface here. Don’t forget to check the official documentation to learn more about TypeScript features.

Edit this page Tweet this article