Introduction to TypeScript

Published in WordPress.

Watch our video

There is a better version of your web

Share this post

The first languages I’ve ever programmed with are C and Java. With these languages, every time you define a variable you have to specify its type. If you try to assign it a value of another type, the compiler will complain:

// main.c
int main() {
  int x = "two";
  return 0;
}

> cc main.c -o main
// warning: initialization makes integer from pointer without a cast

The problem is that, due to my lack of experience, many of the complaints I received from the compiler looked cryptic and complex. That’s why, in the end, I looked at the compiler as a tool that was limiting my creativity, when in reality it’s supposed to be a partner who is there to help.

Later on in my career, I started to use some programming languages without strong typing, like JavaScript or PHP. I thought they were pretty cool: it was extremely easy to prototype things quickly without having to deal with a picky compiler.

As you already know, the programming languages on which WordPress is based are PHP and JavaScript. This means you’re probably used to code your plugins and themes without a compiler watching your back. It’s only you, your skills, and your creativity. Well, and errors like this one:

const user = getUser( userId );
greet( user.name );
// Uncaught TypeError: user is undefined

If you’re sick of undefined-like bugs in your code, it’s time to add a compiler to your workflow. Let’s take a look at what TypeScript is and how it allows us to improve the quality of our software by several orders of magnitude.

What is TypeScript

TypeScript is a JavaScript-based programming language that was created with the aim of adding strong and static typing. TypeScript types allow us to describe the shape of our objects and variables, resulting in a better documented and more robust code. TypeScript itself will be responsible for validating everything we do.

As designed, TypeScript is a superset of JavaScript. This means that any code written in plain JavaScript is, by definition, also valid TypeScript. But the reverse is not true: if you use TypeScript-specific features, the resulting code is not valid JavaScript until you transpile it.

How TypeScript Works

To understand how TypeScript works we are going to use its Playground, a small editor where we can write TypeScript code and see what the compiler tells us about it.

Being a superset of JavaScript, writing TypeScript code is extremely easy. Basically the following JavaScript code:

let user = "David";
let age = 34;
let worksAtNelio = true;

console.log( user, age, worksAtNelio );

is also TypeScript code. You can copy and paste it in the TypeScript Playground and you will see that it compiles. So what’s so special about it? Its types, of course. In JavaScript, you can do the following:

let user = "David";
let age = 34;
let worksAtNelio = true;

user = { name: "Ruth" };

console.log( user, age, worksAtNelio );

but that will trigger an error in TypeScript. Just try it out in the Playground and you will see the following error:

Type '{ name: string; }' is not assignable to type 'string'.

So what’s that all about?

TypeScript can infer the type of a variable automatically. That means we don’t need to explicitly tell it “hey, this variable is a string” (or a number, or a boolean, or whatever); instead, it looks at the value it’s been first given and infers its type based on that.

In our example, when we defined the variable user we assigned it the text string "David", so TypeScript knows that user is (and should always be) a string. The problem is that a little later we try to change the type of our user variable by assigning it an object that has a single property (name) whose value is the string "Ruth". Clearly, this object is not a string, so TypeScript complains and tells us that the assignment cannot be performed.

There’s two possible routes to fix this:

// Option 1
let user = "David";
user = "Ruth"; // OK.

// Option 2
let user = { name: "David" };
user = { name: "Ruth" };

Let’s be explicit when defining the types of our variables

But type inference is only there to help us. If we want to, we can explicitly tell TypeScript a variable’s type:

let user: string = "David";
let age: number = 34;
let worksAtNelio: boolean = true;

console.log( user, age, worksAtNelio );

which renders the exact same result as the types inferred by TypeScript. Now this begs the question: if TypeScript can infer types automatically, why do we need this feature?

On the one hand, specifying types explicitly can serve to better document our code (which will become even clearer in the next section). On the other hand, it allows us to solve the limitations of the type inference system. Yup, you read that right: there are cases in which the inference is not very good and the best TypeScript can do is tell us that “well, I don’t know what this variable is supposed to be exactly; I guess it can be anything!”

Consider the following example:

const getName = ( person ) => person.name;
let david = { name: "David", age: 34 };
console.log( getName( david ) );

// Parameter 'person' implicitly has an 'any' type.

In this case, we are defining a getName function that receives a parameter and returns a value. Notice how many things we’re assuming in such a simple fuction: we’re expecting an object (person) with at least one property called name. But we don’t really have any guarantee that whoever calls this function will use it propertly. But even if they do, we still don’t know what the resulting type of this function is. Sure, you might assume that a name will be a string, but you only know that because you’re a human and you understand the meaning of that word. But look at the following examples:

getName( "Hola" ); // Error
getName( {} ); // Returns undefined
getName( { name: "David" } ); // Returns a string
getName( { name: true } ); // Returns a boolean

everytime we call the function, we get a different result! So, despite TypeScript watching our backs, we’re still running into classic JavaScript issues: this function can receive anything and it can return anything.

The solution, you guessed it, is to explicitly annotate the function.

Define your own types

But before we do, let’s see another additional TypeScript feature: custom data types. So far, almost all our examples used basic types like string, number, boolean, etc, which I think are pretty easy to understand. We’ve also seen more complex data structures, like objects:

let david = { name: "David", age: 34 };

but we didn’t pay much attention to its type as inferred by TypeScript, did we? All we’ve seen is an example of an invalid assignment due to a typing mismatch:

let user = "David";
user = { name: "Ruth" };
// Type '{ name: string; }' is not assignable to type 'string'.

which somehow hinted that the type of the object was “{name:string;},” which you can read as “this is an object with a property called name of type string.” If that’s how object types are defined, then the following should be valid TypeScript:

let david: { name: string, age: number } =
  { name: "David", age: 34 };

and it is indeed. But I’m sure you’ll agree it’s anything but convenient. Luckily, we can create new types in TypeScript.

In general, a custom type is basically a regular TypeScript type with a custom name:

type Person = {
  name: string;
  age: number;
};

let david: Person = { name: "David", age: 34 };

Cool, huh? Now that we have the Person type, we can easily annotate our getName function and clearly specify the type of our input parameter:

const getName = ( person: Person ) => person.name;

And this simple update gives TypeScript a lot of information! For example, it can now infer that the result type of this function is a string, because it knows for sure that the name attribute of a Person object is also a string :

let david: Person = { name: "David", age: 34 };
let davidName = getName( david );

davidName = 2;
// Type 'number' is not assignable to type 'string'.

But, as always, you can explicitly annotate the result type if you want to:

const getName = ( person: Person ): string =>
  person.name;

More stuff with TypeScript types…

Finally, I wanted to share with you a couple of interesting feats you can benefit from if you define your own types in TypeScript.

TypeScript is very demanding when it comes to assigning values to variables. If you have explicitly defined the type of a certain variable, you can only assign values that exactly match that type. Let’s see it with several examples:

type Person = {
  name: string;
  age: number;
};

let david: Person = { name: "David", age: 34 };
// OK

let ruth: Person = { name: "Ruth" };
// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'

let toni: Person = { name: "Toni", age: 35, gender: "M" };
// Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

As you can see, a Person variable only accepts objects that fully comply with the Person type. If some attributes are missing (ruth doesn’t have an age) or there’s more attributes than those included in the type (toni has a gender), TypeScript will complain and trigger a type mismatch error.

But, if we’re talking about invoking functions, things are different! Argument types in a function aren’t an exact requirement, but specify the minimum interface the parameters must comply with. I know, I know, that’s too abstract. Let’s take a look at the following example, which I believe will make things clearer:

type NamedObject = {
  name: string;
};

const getName = ( obj: NamedObject ): string =>
  obj.name;

const ruth = { name: "Ruth" }
getName( ruth );
// OK

const david = { name: "David", age: 34 };
getName( david );
// OK

const toni = { firstName: "Toni" };
getName( toni );
// Argument of type '{ firstName: string; }' is not assignable to parameter of type 'NamedObject'. Property 'name' is missing in type '{ firstName: string; }' but required in type 'NamedObject'.

As you can see, we have redefined the getName function as something a little more generic: it now takes a NamedObject object, that is, an object that must have an attribute called name of type string. Using this definition, we see how ruth and david perfectly match this requirements (they both have a name attribute), but toni doesn’t, since it doesn’t have the expecte attribute name.

Conclusion

TypeScript is a programming language that extends JavaScript by adding static type definitions. This allows us to be much more precise when defining the data we work with and, more importantly, it helps us detect errors earlier.

The cost of integrating TypeScript into a development stack is relatively small and can be done gradually. Since all JavaScript code is, by definition, TypeScript, switching from JavaScript to TypeScript is automatic – you can add types and embellish your code one step at a time.

If you liked this post and want to know more, please share it and let me know in the comment section below.

Featured Image by King’s Church International on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked: •

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.