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;
}
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;
}
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!"
};
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
};
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 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");
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.
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);
});
}
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);
});
}
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 'ILink'.
No index signature with a parameter of type 'string' was found on type 'ILink'.
Here's the offending line:
return arrayElement[lookupKey].match(regex);
"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;
}
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 ILink {
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 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;
}
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);
});
}
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' } ]
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();
}
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]
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 */
}
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");
Now compile it:
npm run tsc
and check out the error:
error TS2322: Type 'string' is not assignable to type 'ILink[]'.
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)