TypeScript Tutorial For Beginners: Your Friendly Guide

What is TypeScript and why you may want to use it? Learn more with this TypeScript tutorial for beginners and start adding types to your JavaScript code!

TypeScript Tutorial For Beginners

TypeScript tutorial for beginners: who this guide is for

The following guide is a TypeScript tutorial for JavaScript developers interested in learning more about TypeScript. That means a decent knowledge of "vanilla" JavaScript is appreciated, even though I'll give you pointers to the fundamentals as we go.

Do the words TypeScript and "beginners" belong in the same tutorial? I was not sure before writing this guide but every day I see a lot of beginners interested in TypeScript. If you decide to do so, be aware that learning TypeScript in your early days side by side with JavaScript will be hard. But it'll pay off in the long run. Keep going! If that's your case you're welcome to continue reading.

Before starting off make sure to have one of the latest version of Node.js installed on your system.

And now enjoy the reading!

TypeScript tutorial for beginners: what is TypeScript?

The definition from the official website says: "a typed superset of JavaScript" but it assumes you know what a "superset" is and what "typed" means. Instead to keep things simple you can think of TypeScript as of "a layer on top" of JavaScript.

TypeScript is a layer because you can write TypeScript code in your editor. After a compilation all that TypeScript stuff is gone and you're left with plain, simple JavaScript.

If the idea of a compilation step confuses you keep in mind that JavaScript is already compiled and then interpreted. There is a JavaScript engine that reads and executes your code.

But JavaScript engines are not able to read TypeScript code so any TypeScript file should go under a "pre-translation" process, called compilation. Only after the first compilation step you're left with pure JavaScript code, ready to run in a browser. You'll see later how the TypeScript compilation is done.

For now let's keep in mind that TypeScript is a special kind of JavaScript but it needs a "translator" before running in a browser.

TypeScript tutorial for beginners: why TypeScript?

At first you won't understand exactly why TypeScript makes sense, after all it gets stripped down before becoming JavaScript code. "What's the point of TypeScript" you'll ask. That's a good question my friend.

In reality you'll see its benefits as soon as it will catch serious and silly mistakes in your code. More important your codebase will become well structured and almost self-documenting. You'll also appreciate improved autocompletion in your editor but that's just a nice side effect.

Anyway, every now and then a new thread pops up on Twitter or on the "orange website" saying that TypeScript is useless (the TypeScript tax) or too awkward.

As with almost everything in IT there are partisans on both sides of the barricade. There are detractors and proponents for TypeScript but what matters is that TypeScript is a solid tool and having it in your tool belt won't make harm.

My goal here is to show the tool and help you forming your own idea on TypeScript.

TypeScript tutorial for beginners: setting up TypeScript

Setting up? Why so? Isn't TypeScript just a language? Kind of. TypeScript has also a binary which compiles TypeScript code to JavaScript code. Remember, browsers do not understand TypeScript. Let's install the binary then. Create a new Node project inside a new folder:

mkdir typescript-tutorial && cd $_
npm init -y

and then install TypeScript with:

npm i typescript --save-dev

Next up configure a Node script so we can run the TypeScript compiler easily:

  "scripts": {
    "tsc": "tsc"
  },

tsc stands for TypeScript compiler and whenever the compiler runs it will look for a file named tsconfig.json in the project folder. Let's generate a configuration file for TypeScript with:

npm run tsc -- --init

If everything goes well you'll get "message TS6071: Successfully created a tsconfig.json file." and you'll see the new file in the project folder. Now, keep calm. tsconfig.json is a scary configuration file. You don't need to know every single gist of it. In the next section you'll see just the relevant bits for getting started.

TypeScript tutorial for beginners: configuring the TypeScript compiler

It's a good idea to initialize a git repo and commit the original tsconfig.json before touching the file. We'll leave just some of the configuration options and remove everything else. Later you may want to compare your version with the original. For starting off open up tsconfig.json and replace all the original content with the following:

{
  "compilerOptions": {
    "target": "es5",
    "strict": true
  }
}

Save and close the file. First of all you may wonder what tsconfig.json is for. This configuration file is read by the TypeScript compiler and by any code editor with TypeScript support.

TypeScript compiles down to "vanilla" JavaScript. The key target determines the desired JavaScript version, ES5 (or a newest release).

Depending on the level of "strictness" for tsconfig.json the compiler and the editor will comply if you don't add the appropriate type annotations to your code (more on this in a minute).

With strict set to true TypeScript enforces the maximum level of type checks on your code enabling amongst the other:

  • noImplicitAny true: TypeScript complains when variables do not have a defined type
  • alwaysStrict true: strict mode is a safe mechanism for JavaScript which prevents accidental global variables, default this binding, and more. When alwaysStrict is set true TypeScript emits "use strict" at the very top of every JavaScript file.

There are a lot more configuration options available. With time you'll learn more, for now the two options above are everything you need to know for getting started. But what is "any" by the way?

A couple of words on "types"

By now you should've got an hint of what TypeScript does. Everything revolves around types. These are not the classic JavaScript "types" like String, Object, Boolean. TypeScript adds more type on its own like any (and more).

any in particular is a "loose" TypeScript type. It means: this variable might be of any type: string, boolean, object, really, I don't care. Which in fact is like having no type checking at all. With strict set to true instead you say to TypeScript "don't allow ambiguity in my code".

For this reason I recommend keeping the max level of strictness on TypeScript, even if it can be harder to fix all errors at first. And now we're almost ready to see TypeScript in action!

TypeScript tutorial for beginners: TypeScript in action

Everything begins with a legitimate (apparently) JavaScript function: filterByTerm. Create a new file named filterByTerm.js in your project folder and copy the following code into it:

function filterByTerm(input, searchTerm) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("inputArr cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm("input string", "java");

Don't worry if you don't understand the logic right now. Take a look at the parameters of that function and at how they are used a couple of lines later. Just by looking at the code you should have already spotted the problem (no it's not Java).

I'm wondering if there is a way to check that function in my IDE, without running the code or having to test it with Jest. Is that even possible? TypeScript is great for that, in fact it's one the best tool for static checking in JavaScript, that is, "testing" the correctness of your code before it even runs.

So take the leap into the TypeScript world and change the extension of your file from filterByTerm.js to filterByTerm.ts. With this change you're going to uncover a bunch of errors in your code:

From JavaScript to TypeScript

Can you see those red marks under function parameters? From now on I'll show you errors in textual form, but keep in mind that IDEs and text editors display these red line whenever you make a mistake in TypeScript.

To confirm we're doing something wrong run:

npm run tsc

and take a look at the errors:

filterByTerm.ts:1:23 - error TS7006: Parameter 'input' implicitly has an 'any' type.

filterByTerm.ts:1:30 - error TS7006: Parameter 'searchTerm' implicitly has an 'any' type.

filterByTerm.ts:5:32 - error TS7006: Parameter 'arrayElement' implicitly has an 'any' type.

Bingo! TypeScript is telling you that function parameters have the any type, which if you recall can be any kind of type in TypeScript. We need to add the appropriate type annotations to our TypeScript code.

But wait, what's a type, really?

What are types and what's wrong with JavaScript?

JavaScript has types and if you worked with the language before you know there are strings, booleans, numbers, objects, and so on. As of today there are eight types in JavaScript:

  • String
  • Number
  • BigInt
  • Boolean
  • Null
  • Undefined
  • Object
  • Symbol

Everything in that list is a "primitive" except Object which is a type. Every JavaScript type has a corresponding representation which can be used in our code, like strings and numbers for example:

var name = "Hello John";
var age = 33;

The "problem" with JavaScript is that a variable can change its type whenever it (or we) wants. A boolean for example can later become string (save the following code in a file named types.js):

var aBoolean = false;
console.log(typeof aBoolean); // "boolean"

aBoolean = "Tom";
console.log(typeof aBoolean); // "string"

The transformation can either be intentional, a developer might really want to assign "Tom" to aBoolean, but there are high chances that these kind of errors will happen by accident.

Now, technically speaking there's nothing wrong with JavaScript itself because its "type dynamism" is intentional. JavaScript was born as a simple scripting language for the web, not as a fully fledged enterprise language.

JavaScript relaxed nature however can pose serious problems in your code and undermine its maintainability. TypeScript aims to solve these problems by adding strong types to JavaScript. In fact if you change the extension of types.js to types.ts you'll see TypeScript complaining in the IDE.

The compilation of types.ts will produce:

types.ts:4:1 - error TS2322: Type '"Tom"' is not assignable to type 'boolean'.

Armed with this knowledge let's dig deeper into TypeScript types.

Dipping our toes into TypeScript types

TypeScript revolves around types and looks like our code has no types at all. Time to add some. We're going to fix function parameters first. By looking at how the function is called it seems it takes two strings as arguments:

filterByTerm("input string", "java");

Are we sure? Let's add your first type annotation to the function. Here's how:

function filterByTerm(input: string, searchTerm: string) {
    // omitted
}

// omitted

That's it! By adding types to the parameters we're migrating our code from pure JavaScript to TypeScript. But if you try to compile the code:

npm run tsc

here's what happens:

filterByTerm.ts:5:16 - error TS2339: Property 'filter' does not exist on type 'string'.

Can you see how TypeScript is guiding you? The problem is with the filter function:

function filterByTerm(input: string, searchTerm: string) {
    // omitted
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

We're telling TypeScript that "input" is a string but later in the code we call the filter method on it, which belongs to arrays. What we really want instead is marking "input" as an array of something, maybe an array of strings?

For doing so you have two options. Option 1 with string[]:

function filterByTerm(input: string[], searchTerm: string) {
    // omitted
}

or if you like this syntax, option 2 with Array<string>:

function filterByTerm(input: Array<string>, searchTerm: string) {
    // omitted

}

Personally I prefer option 2. Now let's try to compile again (npm run tsc) and here it is:

filterByTerm.ts:10:14 - error TS2345: Argument of type '"input string"' is not assignable to parameter of type 'string[]'.

filterByTerm("input string", "java");

TypeScript doesn't want to leave us alone I suppose. Don't blame it, we marked input as an array of strings and now we're trying to pass in a string. That's an easy fix! Let's pass an array of strings instead:

filterByTerm(["string1", "string2", "string3"], "java");

And here's the complete code so far:

function filterByTerm(input: Array<string>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(["string1", "string2", "string3"], "java");

Looks good to me. But if you compile it's not (npm run tsc):

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'string'.

Ok TypeScript, fair enough. We're passing in an array of strings but later in the code we try to access a property named "url":

return arrayElement.url.match(regex);

That means we want an array of objects, not an array of strings. Let's fix that into the next section!

TypeScript tutorial for beginners: TypeScript objects and interfaces

We left with TypeScript complaining (what a surprise) because filterByTerm has been passed an array of strings. "url" property does not exists on type string TypeScript yelled. Let's help TypeScript then by passing an array of objects, where every object has the required url property:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);

and while you're there update the function signature so that it takes an array of objects:

function filterByTerm(input: Array<object>, searchTerm: string) {
    // omitted
}

Now let's compile the code:

npm run tsc

and admire the output:

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'object'.

Here we go again! It makes sense, at least in TypeScript: the generic JavaScript object does not have any property named "url". And for me this is where TypeScript really starts to shine.

The point is, you can’t assign properties to a random object and call it a day. TypeScript expects that every entity in your code conforms to a particular shape. And that shape in TypeScript has a name: the interface.

Now, at first it will look like alien syntax, but once you get accustomed to interfaces you'll start to use them all over the place. But what's an interface by the way? An interface in TypeScript is like a contract. Or put it another way an interface is like a "model" for your entity.

By taking a look at our code we can think of a simple "model" named Link for an object whose shape should conform to the following pattern:

  • it must have a url property of type string

In TypeScript you would define that "model" with an interface, like so (put the following code at the top of filterByTerm.ts:

interface Link {
  url: string;
}

With the interface declaration we say "I want to use that shape in my TypeScript code from now on". That's not valid JavaScript syntax of course and it will be stripped away during compilation.

Now we can use our interface, which is actually also a custom TypeScript type, by fixing the parameter "input":

function filterByTerm(input: Array<Link>, searchTerm: string) {
    // omitted
}

With this fix we say to TypeScript "expect an array of Link" as the input for that function. Here's the complete code:

interface Link {
  url: string;
}

function filterByTerm(input: Array<Link>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);

At this point all the errors should go away and you can run:

npm run tsc

The compilation step will produce a file named filterByTerm.js with plain JavaScript code in the project folder. You can check out the file and see how TypeScript specific declaration are stripped away.

Since "alwaysStrict" is set true the TypeScript compiler also emits "use strict" at the top of filterByTerm.js.

Great job on your first TypeScript code! In the next section we'll explore interfaces a bit more.

TypeScript tutorial for beginners: interfaces and fields

TypeScript interfaces are one of the most powerful construct of the language. Interfaces help in shaping "models" across your application so that any developer can pick that shape and conform to it when writing code.

So far we defined a simple interface, Link:

interface Link {
  url: string;
}

If you want to add more fields to the interface it's a matter of declaring them inside the block:

interface Link {
  description: string;
  id: number;
  url: string;
}

Now any object of type Link must "implement" the new fields, otherwise you get an error. In fact by compiling the code with:

npm run tsc

TypeScript screams at you:

filterByTerm.ts:17:4 - error TS2739: Type '{ url: string; }' is missing the following properties from type 'Link': description, id

The problem is with the argument of our function:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);

TypeScript is able to deduct by looking at the function declaration that the argument is of type Array of Link. Thus any object inside that array must have (implement) all the fields defined in the interface Link.

Most of the time that's far from optimal. After all we don't know if every new object of type Link will ever have all the fields. Worry not, to make the compilation pass we can declare interface's fields optional with a question mark:

interface Link {
  description?: string;
  id?: number;
  url: string;
}

Now both the editor and the compiler will be fine. Yet TypeScript interfaces can do a lot more, in the next sections we'll see how to extend them. But first a brief note about variables in TypeScript.

TypeScript tutorial for beginners: typing variables

So far you've seen how to add types to function's parameters:

function filterByTerm(input: Array<Link>, searchTerm: string) {
    //
}

TypeScript is not limited to that, of course you can also add types to any variable. To illustrate the concept let's extract function's arguments one by one. First I'm going to extract every single object:

const obj1: Link = { url: "string1" };
const obj2: Link = { url: "string2" };
const obj3: Link = { url: "string3" };

Notice how I can say to TypeScript that obj1, obj2 and obj3 are of type Link. In "vanilla" JavaScript you would write:

const obj1 = { url: "string1" };
const obj2 = { url: "string2" };
const obj3 = { url: "string3" };

Next up we can define an array of Link like so:

const arrOfLinks: Array<Link> = [obj1, obj2, obj3];

And finally the search term:

const term: string = "java";

Here's the complete code:

interface Link {
  description?: string;
  id?: number;
  url: string;
}

function filterByTerm(input: Array<Link>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

const obj1: Link = { url: "string1" };
const obj2: Link = { url: "string2" };
const obj3: Link = { url: "string3" };

const arrOfLinks: Array<Link> = [obj1, obj2, obj3];

const term: string = "java";

filterByTerm(arrOfLinks, term);

Ok, I feel you. TypeScript looks more verbose and sometimes redundant compared to JavaScript. But with time you'll see that the more you add types, the more your code becomes robust.

The more you help TypeScript to understand the intent of your code by adding type annotations, the more you'll be fine later. And your developer experience will skyrocket.

For example now that arrOfLinks is associate with the correct type (array of Link), your editor is able to infer that every object in the array has a property named url, as defined in the interface Link:

Typing variables

Now tell me this isn't fantastic because indeed it is. TypeScript has a lot more types besides string, Array and number.

There are booleans, tuples, "any", never, enums. With time you'll learn them all. If you're curious check out the documentation for the basic types.

Now let's move on to extending interfaces.

(Most of the times Typescript will be able to infer types on its own. As a rule of thumb let it do the job for you!)

TypeScript tutorial for beginners: extending interfaces

TypeScript interfaces are great. There will come a time however when you'll need a new entity in your code which happens to be almost the same as another existing interface. Let's say for example we want a new interface named TranslatedLink with the following properties:

  • id, number
  • url, string
  • description, string
  • language, string

Description, id, and url ... looks like we already have the Link interface with those very same properties:

interface Link {
  description?: string;
  id?: number;
  url: string;
}

Is there a way to reuse the interface Link? Turns out in TypeScript we can extend an interface by assigning its properties to a new interface, so that TranslatedLink for example "inherits" some traits from Link. Here's how to do it, notice the keyword extends:

interface Link {
  description?: string;
  id?: number;
  url: string;
}

interface TranslatedLink extends Link {
  language: string;
}

Now any object of type TranslatedLink will have the optional properties description, id, url, and the new property "language":

interface Link {
  description?: string;
  id?: number;
  url: string;
}

interface TranslatedLink extends Link {
  language: string;
}

const link1: TranslatedLink = {
  description:
    "TypeScript tutorial for beginners is a tutorial for all the JavaScript developers ...",
  id: 1,
  url: "www.valentinog.com/typescript/",
  language: "en"
};

When an object like link1 uses an interface we say that link1 implements the properties defined in that interface. The interface on the other hand has implementations when it's used for describing one or more objects in your code.

Extending an interface means borrowing its properties and widening them for code reuse. But wait, there's more! As you'll see soon TypeScript interfaces can also describe functions.

But first let's take a look at indexing!

TypeScript tutorial for beginners: the indexing interlude

JavaScript objects are containers for key/value pairs. Imagine a simple object:

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

We can access the value of any key with the dot syntax:

console.log(paolo.city);

or with the bracket syntax (the same is true for JavaScript arrays since arrays are a special kind of object):

console.log(paolo["city"]);

Now imagine that the key becomes dynamic, so that we can put it in a variable and reference it inside brackets:

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const key = "city";

console.log(paolo[key]);

Now let's add another object, put both inside an array, and filter the array with the filter method like we did in filterByTerm.js. But this time the key is passed dynamically so becomes possible to filter by any key:

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const tom = {
  name: "Tom",
  city: "Munich",
  age: 33
};

function filterPerson(arr, term, key) {
  return arr.filter(function(person) {
    return person[key].match(term);
  });
}

filterPerson([paolo, tom], "Siena", "city");

Here's the relevant line:

return person[key].match(term);

Will it work? Yes because JavaScript does not care whether paolo or tom are "indexable" with a dynamic key. And what about TypeScript? Will it give an error in this case?

Let's find out: in the next section we'll make filterByTerm more dynamic with a variable key.

Interfaces can have indexes

Let's get back to filterByTerm.ts and in particular to our filterByTerm function:

function filterByTerm(input: Array<Link>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

It does not look so flexible because for every Link we match the hardcoded property "url" against a regular expression. We may want to make the property, thus the key, dynamic. Here's a first try:

function filterByTerm(
  input: Array<Link>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}

lookupKey is the dynamic key, which gets also assigned a default parameter as a fallback, the string "url". Let's compile the code:

npm run tsc

Does it compile?

error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Link'.
  No index signature with a parameter of type 'string' was found on type 'Link'.

Here's the offending line:

return arrayElement[lookupKey].match(regex);

"No index signature". Wow. It's an "easy" fix. Head over the interface Link and add the index:

interface Link {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string;
}

The syntax is kind of weird but is similar to the dynamic key access on our objects. It means we can access any key of that object through an index of type string, which in turn returns another string.

Anyway, that first try will make other errors pop up like:

error TS2411: Property 'description' of type 'string | undefined' is not assignable to string index type 'string'.
error TS2411: Property 'id' of type 'number | undefined' is not assignable to string index type 'string'.

That's because some properties on the interface are optional, maybe undefined, and not always of type string (id is a number for example).

We can try to fix the problem with a union type, a TypeScript syntax for defining types that are the union between two or more other types:

interface Link {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string | number | undefined;
}

The following line:

[index: string]: string | number | undefined;

means that index is a string and may return another string, a number, or undefined. Try to compile again and here's another error:

error TS2339: Property 'match' does not exist on type 'string | number'.
return arrayElement[lookupKey].match(regex);

Makes sense. The match method works only for strings and there's a chance that our index will return a number. To fix the error we can use anyas a workaround:

interface Link {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}

The TypeScript compiler is happy again, but at what cost?

Now it's time to turn our attention to another fundamental TypeScript feature: return types for functions.

TypeScript tutorial for beginners: return types for functions

It's been a lot of new stuff up until now. Anyway, I skipped over another TypeScript's useful feature: return types for functions.

To understand why it's handy to add a type annotation for return values imagine me, messing with your fancy function. Here's the original version:

function filterByTerm(
  input: Array<Link>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}

If called as it is, passing in the array of Link you saw earlier and the search term "string3", it should return an array of objects, as expected:

filterByTerm(arrOfLinks, "string3"); 

// EXPECTED OUTPUT:
// [ { url: 'string3' } ]

But now consider an altered variant:

function filterByTerm(
  input: Array<Link>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}

If called now, with the same array of Link and the search term "string3", it returns ... [object Object]`!:

filterByTerm(arrOfLinks, "string3");

// WRONG OUTPUT:
// [object Object]

Can you spot the problem? Hint: toString.

The function is not working as expected and you would never know unless hitting production (or testing your code). Luckily TypeScript can catch these errors, as you write in the editor.

Here's the fix (just the relevant portion):

function filterByTerm(/* omitted for brevity */): Array<Link> {
 /* omitted for brevity */
}

How it works?By adding type annotations before the function's body we tell TypeScript to expect another Array as the return value. Now the bug can be spotted easily. Here's the code so far (altered version):

interface Link {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}

function filterByTerm(
  input: Array<Link>,
  searchTerm: string,
  lookupKey: string = "url"
): Array<Link> {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}

const obj1: Link = { url: "string1" };
const obj2: Link = { url: "string2" };
const obj3: Link = { url: "string3" };

const arrOfLinks: Array<Link> = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");

Now compile it:

npm run tsc

and check out the error:

error TS2322: Type 'string' is not assignable to type 'Link[]'.

Fantastic. We're expecting an array of links, not a string. To fix the error remove .toString() from the end of filter and compile the code again. It should work now!

We added another layer of protection to our code. Granted, that bug could have been spotted with a unit test. TypeScript is a nice layer of safety rather than a complete replacement for testing.

Let's continue the exploration with type aliases!

TypeScript tutorial for beginners: type aliases vs interfaces

So far we've seen the interface as a tool for describing objects and custom types. But lurking through other's people code you might also have noticed the keyword type.

Apparently interface and type are used interchangeably in TypeScript, but they're different in many ways. And that's cause of confusion for TypeScript beginners.

Remember: an interface in TypeScript is the shape of something, most of the times a complex object.

A type on the other hand might also be used to describe a custom shape, but it's just an alias, or put it another way, a label for a custom type. For example, let's imagine an interface with a couple of fields, one of them being a union type of boolean, number, and string:

interface Example {
  authenticated: boolean | number | string;
  name: string;
}

With a type alias you can extract that custom union type for example, and create a label named Authenticated:

type Authenticated = boolean | number | string;

interface Example {
  authenticated: Authenticated;
  name: string;
}

This way you can isolate what changes, so you don't have to copy/paste the union type all over the codebase.

If you want to apply type to our example (filterByTerm) create a new label named Links and assign Array to it. That way you can reference the type:

// the new label
type Links = Array<Link>;
// the new label

function filterByTerm(
  input: Links,
  searchTerm: string,
  lookupKey: string = "url"
): Links {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}

const obj1: Link = { url: "string1" };
const obj2: Link = { url: "string2" };
const obj3: Link = { url: "string3" };

const arrOfLinks: Links = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");

Now, that's not the most clever example of labelling types but you should get the point. So what to use between interface and type? I prefer interface for complex objects. An approach suggested by the TypeScript doc as well:

Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.

Hope it helped to clarify your doubts.

In the next section we'll take a quick look at two more TypeScript topics before saying goodbye. Keep going!

TypeScript tutorial for beginners: more on interfaces and objects

Functions are first class citizens in JavaScript, yet Object is the most important entity in the language.

Objects are mostly containers for key/value pairs and it should be no surprise that they can also hold functions. When a function lives inside an object it has access to the "host" object through the keyword this:

const tom = {
  name: "Tom",
  city: "Munich",
  age: 33,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};

So far you saw TypeScript interfaces applied to simple objects for describing strings and numbers. But they can do a lot more. Let's make an example. Create a new file named interfaces-functions.ts with the following code:

const tom = {
  name: "Tom",
  city: "Munich",
  age: 33,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};

It's a JavaScript object but it needs types. Let's make an interface to make tom conforming to a well defined shape. How about "IPerson"? And while there let's also apply the new interface to tom:

interface IPerson {
  name: string;
  city: string;
  age: number;
}

const tom: IPerson = {
  name: "Tom",
  city: "Munich",
  age: 33,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};

Compile the code (npm run tsc) and watch it fail:

interfaces-functions.ts:11:3 - error TS2322: Type '{ name: string; city: string; age: number; printDetails: () => void; }' is not assignable to type 'IPerson'.
  Object literal may only specify known properties, and 'printDetails' does not exist in type 'IPerson'.

Cool, IPerson does not have any property named printDetails but more important it should be a function. Luckily TypeScript interfaces can also describe functions. Here's how:

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): void;
}

Here we added a property printDetails of type function, returning void. void is useful as a return value for functions that ... don't return anything at all.

A function that prints to console in fact does not return anything. If we were to return a string from printDetails we could adjust the return type to string:

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
}

const tom: IPerson = {
  name: "Tom",
  city: "Munich",
  age: 33,
  printDetails: function() {
    return `${this.name} - ${this.city}`;
  }
};

Now, what if the function has parameters? In the interface you can add type annotations for them:

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
  anotherFunc(a: number, b: number): number;
}

and if you start typing "an..." inside an object implementing IPerson, most IDE will auto complete the function for you. Developer productivity at its finest.

What's missing from this guide?

For obvious reasons I couldn't cover every single TypeScript feature here. For example I intentionally left off ES2015 classes and their relation with interfaces or more advanced types like Partial.

I'll cover more TypeScript on future posts but if you're in a hurry head over the TypeScript doc. The official TypeScript documentation is not so friendly but it should be easier for you to dig deeper into it after reading my guide.

Conclusions and resources

What a journey! Thanks for reading and great job on following the guide, I hope you're now ready to use TypeScript in your projects! Feel free to come back here or bookmark the page if you need an handy guide to TypeScript.

In this TypeScript tutorial you learned:

  • type annotations for variables, function parameters, and return values
  • interfaces
  • custom types
  • type aliases

You've seen TypeScript saving my JavaScript code from silly errors, ranging from wrong argument types to malformed return values. It bears repeating that TypeScript is not a replacement for testing. It is indeed a valuable tool, hard to grasp at first but totally worth the investment (like my old friend Redux).

If you want to stay updated on TypeScript's evolution I suggest following a couple of blogs:

The official TypeScript blog where you can learn about the new releases and features.

Marius Schulz blog**, a software engineer passionate about all the things TypeScript. He also runs a TypeScript weekly newsletter.

After reading my TypeScript tutorial you can also continue exploring the language with the TypeScript book, a fantastic, free resource.

Thanks again for reading and stay tuned!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!