DEV Community

Cover image for TypeScript Tutorial For Beginners: The Missing Guide - Part 3
Valentino Gagliardi
Valentino Gagliardi

Posted on • Originally published at dev.to

TypeScript Tutorial For Beginners: The Missing Guide - Part 3

Just crossed 5k follower on dev.to! Thank you everyone! What a fantastic community! Who's on Twitter too? Let's connect => I'm here.

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!

Originally published on valentinog.com/blog

In this episode:

  • interfaces extends
  • indexing for interfaces
  • typing return values

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 we want a new interface named IPost with the following properties:

  • id, number
  • title, string
  • body, string
  • url, string
  • description, string

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

interface ILink {
  description?: string;
  id?: number;
  url: string;
}
Enter fullscreen mode Exit fullscreen mode

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

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

interface IPost extends ILink {
  title: string;
  body: string;
}
Enter fullscreen mode Exit fullscreen mode

Now any object of type IPost will have the optional properties description, id, url, and the required properties title and body:

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

interface IPost extends ILink {
  title: string;
  body: string;
}

const post1: IPost = {
  description:
    "TypeScript tutorial for beginners is a tutorial for all the JavaScript developers ...",
  id: 1,
  url: "www.valentinog.com/typescript/",
  title: "TypeScript tutorial for beginners",
  body: "Some stuff here!"
};
Enter fullscreen mode Exit fullscreen mode

When an object like post1 uses an interface we say that post1 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
};
Enter fullscreen mode Exit fullscreen mode

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

console.log(paolo.city);
Enter fullscreen mode Exit fullscreen mode

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

console.log(paolo["city"]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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 filtering any object key becomes possible:

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");
Enter fullscreen mode Exit fullscreen mode

Here's the relevant line:

return person[key].match(term);
Enter fullscreen mode Exit fullscreen mode

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.

TypeScript tutorial for beginners: interfaces can have indexes

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

function filterByTerm(input: Array<ILink>, 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);
  });
}
Enter fullscreen mode Exit fullscreen mode

It does not look so flexible because for every ILink 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<ILink>,
  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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Does it compile?

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

Here's the offending line:

return arrayElement[lookupKey].match(regex);
Enter fullscreen mode Exit fullscreen mode

"No index signature". Wow. JavaScript is loose while TypeScript on the other hand does care and requires you to add an index to the object's interface. It's an "easy" fix.

Head over the interface ILink and add the index:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string;
}
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

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 ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string | number | undefined;
}
Enter fullscreen mode Exit fullscreen mode

The following line:

[index: string]: string | number | undefined;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 for good we can use the any type without any (no pun intended) regret:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}
Enter fullscreen mode Exit fullscreen mode

The TypeScript compiler is happy again! Yes!

And 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<ILink>,
  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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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

filterByTerm(arrOfLinks, "string3"); 

// EXPECTED OUTPUT:
// [ { url: 'string3' } ]
Enter fullscreen mode Exit fullscreen mode

But now consider an altered variant:

function filterByTerm(
  input: Array<ILink>,
  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();
}
Enter fullscreen mode Exit fullscreen mode

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

filterByTerm(arrOfLinks, "string3");

// WRONG OUTPUT:
// [object Object]
Enter fullscreen mode Exit fullscreen mode

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<ILink> {
 /* omitted for brevity */
}
Enter fullscreen mode Exit fullscreen mode

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 ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
): Array<ILink> {
  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: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

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

filterByTerm(arrOfLinks, "string3");
Enter fullscreen mode Exit fullscreen mode

Now compile it:

npm run tsc
Enter fullscreen mode Exit fullscreen mode

and check out the error:

error TS2322: Type 'string' is not assignable to type 'ILink[]'.
Enter fullscreen mode Exit fullscreen mode

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 in fact is a nice layer of safety rather than a complete replacement for testing.

Let's continue the exploration with type aliases!


Stay tuned for part 4!

Top comments (0)