TypeScript introduction

If you have read some of my previous posts or any kind of JS blog for that matter, chances are you've heard about TypeScript. For those who haven't - TS (TypeScript official abbreviation) is nothing more than modern JavaScript with the addition of static type system. And, it's gaining a lot of attention recently. That's mainly because of the advantages it provides over standard, dynamically-typed JS. So, in this article, I'll guide you in making your first steps with TypeScript assuming you already know JavaScript. It's going to be an in-depth series covering almost everything from pure basics to some complexities. I'll try to explain it all as simple as I possibly can (pros out there - please don't accuse me of oversimplifying stuff πŸ™ƒ). Enjoy! πŸ˜‰


forest trees marked with question marks
Photo by Evan Dennis / Unsplash

Why (not)?

Before we proceed to learn TS, let's first explore why it's worth your effort.

To start, TypeScript is a compiled language that's default compilation target is JavaScript. It's an open source project started and overseen by Microsoft. It provides built-in support for modern ES-Next features and static type system. And, while great many of day-to-day JS developers use e.g. Babel to utilize ES-Next, a concept of static typing might be something new for you (unless you've interacted with other statically-typed languages before 😁).

Static typing makes types of variables known at the compile time. JS is an interpreted or JIT-compiled language with a dynamic type system. But, what's more, important for you like the programming language end user (aka consumer) is what it translates to in real-world use. In this case, static typing brings better error-proneness, and usually much finer IDE support and tooling. Thus, it can greatly improve your coding experience. As for the dynamic type system, it has its advantages too. The main one being not having to specify your types in your code directly. πŸ˜…

There are not much more reasons beyond the mentioned static type system, that would make any kind of big difference. So, if you'd like to learn TS and improve both your knowledge and development experience, follow me and discover the hidden potential of static typing.


Basics

Every feature provided in TS is non-invading, meaning that it has syntax that doesn't overlap with any part of the JS code. This makes porting your JS app back and forth relatively easily.

You can specify your variable's type using the colon (:) followed by the actual name of the type:

const myStringVariable: string = "str";

There are 6 basic, primitive types to remember:

  • number - represents any kind of numeric value - integer or float, hex, decimal, binary etc.
  • string - represents any kind of string value;
  • boolean - represents any boolean value, i.e. true or false;
  • symbol - represents symbol values;
  • null - represents null value only;
  • undefined - represents undefined value only;

This shouldn't be anything new for you. Each one of the above types is properly documented as a part of JS language itself. This fact is only a little bit hidden because of JavaScript's dynamic type system. But, rest assured, TypeScript has much more to offer than just that. And I think we should dig deeper! 🌟


More types!

Object

Starting the list of more complex types, we have the object type. The thing to remember is that it represents any non-primitive value. Which means that primitives are not assignable. That's because almost everything in JS is an object. As you can see, TypeScript greatly respects JavaScript architecture. πŸ˜€

const myObjectVariable: object = "str"; // error
const myObjectVariable2: object = {};

Any

Any type, as the name suggests, indicates any possible value. It serves as a kind-of fallback, allowing you to omit type checking. It's really helpful at the beginning when porting from JS. But, it shouldn't be overused, or even better - it shouldn't be used at all! You don't use TypeScript to just type : any everywhere, do you? πŸ˜‚

let myAnyVariable: any = "str";
myAnyVariable = 10;
myAnyVariable = true;

Void

Void type, with its sounding name, represents the complete absence of type. This is commonly used with functions and tells the compiler that function doesn't return anything. Anything, in this case, includes undefined and null, but who cares? Both of them seemed voided anyway. πŸ˜… For your information, you most likely won't use this type with variables, but have a look at how strange it might be feel like:

let myVoidVariable: void = undefined;
myVoidVariable = null;

Never

Never type, according to the pure definition, represents the value that never occurs. But what exactly does it mean? Basically, it refers to e.g. return type of the function that throw/returns an error, which doesn't allow the function to have a reachable end point. It's also used with, so-called type guards (more about it later). Generally speaking, this type isn't used very often. Anyway, examples will come later, together with some more advanced stuff.


Unknown

Unknown is a relatively new addition to TS types collection - it was introduced in v3.0. It's meant to be a type-safe alternative to any type. How would something like that work? Well, first, any value can be assigned to unknown, just like with any:

const myUnknownVariable: unknown = "str";
const myAnyVariable: any = "str";

The difference appears when it comes to assigning the variable of unknown type to anything else. Here's what I mean:

let myUnknownVariable2: unknown = myUnknownVariable;
myUnknownVariable2 = myAnyVariable;

let myStringVariable: string = "str";
myStringVariable = myAnyVariable;
myStringVariable = myUnknownVariable; // error

Unknown is not assignable to anything but itself...

That's what official TS documentation says and what generally clarifies the difference between any and unknown.


black framed panto-style eyeglasses beside black ballpoint pen
Photo by Dayne Topkin / Unsplash

Composition types

By that moment, we've discovered TS primitive and top types ( that's how these built-in types covered in the above section are called). Now it's time to explore some even more interesting types. Ones that don't always have their direct JS counterparts. I call them composition types because they're composed of some smaller parts. Just to be clear - this name isn't official in any way. 😁


Unions

In a nutshell, unions allow you to specify variable's type that you can assign different types of values to. They function as a list of possible and assignable types. They can be specified by writing your types, divided by the pipe symbol (|).

let myUnionVariable: string | number = "str";
myUnionVariable = 10;
myUnionVariable = false; // error

Union types have incredible potential. You can use them to handle e.g. different types of parameters in functions or replace your any types with these, truly type-safe alternatives.


Literals

Literal types allow you to strictly define the possible value for the variable. Literals themselves aren't any kind of composition types, but they're so often used with e.g. unions and others, that I put them in this category. Now, how a literal type looks like? Well, just like a possible value it annotates:

let myStringLiteral: "str" = "str";
let myNumberLiteral: 10 = 10;
let myBooleanLiteral: true = true;

myStringLiteral = "string"; // error
myNumberLiteral = 1; // error
myBooleanLiteral = false // error

I think that with the example above, you can easily understand the idea behind literal types and that you can imagine just how well these integrate with e.g. unions:

let myVariable: "on" | "off" = "off";
myVariable = "on";
myVariable = "suspend" // error

But what if you want to literally (that's a good word here πŸ™ƒ) express some more complex value like an object? Well, you do exactly the same thing:

let myObjectLiteral: {str: string} = {str: "str"};
myObjectLiteral.str = "string";
myObrjectLiteral.num = 10; // error

Intersection types

Intersection types are closely related to union types. While union types function like logical or, intersection types function like logical and. Thus, you can create them using the and sign (&).

const myIntersectionVariable: {str: string} & {num: number} = {
	str : "str",
	num: 10
};

The created type has properties of all operands. These types are often used with object literals and other complex types and techniques which we'll cover later.


Arrays

After all these types it's time to meet good old arrays. Here, I'll introduce you to the first way of typing an array value. That's because there are two methods of achieving the same goal - more on that later. For now, to denote an array type, you have to write the type for actual values of your array and proceed it by the square brackets symbol ([]).

const myStringArrayVariable: string[] = ["str", "str"]; 

Just to remind you - you can join and use together many of previously met types. You can e.g. create a type for an array of strings and numbers with union types, or create a type for an array of literal values. The options are endless! 🀯

const myUnionArrayVariable: (string | number)[] = ["str", 10];
const myLiteralArrayVariable: ("str")[] = ["str","str"];

I guess that by that moment you already know that in TS additional spaces don't matter. Also, note the round brackets (()) in the above snippet. Just like in normal math (and also JS), they're used to group stuff together. Seems pretty logical. πŸ˜…


Tuples

Structures closely related to arrays, so-called tuples can be utilized to specify a type of an array with a fixed number of elements, with all of them having strictly specified type. Take a look at the example:

const myTupleVariable: [number, string] = [10, "str"];
const myTupleVariable2: [string, number] = [10, "str"]; // error

It explains mostly everything. To define a tuple type, you start with square brackets ([]) that are really characteristic for arrays of any kind, any include types for your tuple one by one, separated by commas. Again, pretty rationale stuff.


Enums

Enums can feel somewhat new to some JS programmers out there. But in truth, these are commonly known among statically programming languages communities. They are used to simply provide more friendly names to numeric values. For example, there's a common pattern for requiring different numbers in configuration objects or etc. That's where enums find their use-cases.

Enums are defined in a bit different way than any types we've met before. Namely, by using enum keyword.

enum Color {Red, Green, Blue};

In the example above we've defined an enum named Color with 3 members - Red, Green, Blue. By default, each these members start numbering from 0, increasing by 1 with every next entry. With that said, by using enums you can access both its member numeric value and name too! Let me show you what I mean. 😁

Color.Red // 0
Color.Blue // 2
Color[1] // "Green"
Color[2] // "Blue"

As you can see, you can easily use enums for standard values. But we're in TS and we're all about types here, so how to use enums as types? Well, easy enough - just like other types!

let myColorEnumVariable: Color = Color.Red;
myColorEnumVariable = 2;
myColorEnumVariable = Color[1]; // error

So, a variable of an enum type can be actually taken as a union of number literals, I think. You can assign to it an appropriate number or value of enum member. No other values are allowed even member's names.

Now, a quick note about enums' members numbering. As I said, by default it starts from 0 and increases by one every next member. But you can actually change that by assigning the value directly.

enum Color {Red, Green = 32, Blue};
Color.Red // 0
Color.Green // 32
Color.Blue // 33

And, in the example above, we've overridden the value of Green member. In this way, the Red value stays the same - 0 as by default, Green is assigned a value of 32, and Blue is 33 due to the rule of increasing by 1.

To summarize, enums are pretty useful when used properly and IMHO, they have one of the hardest or rather newest syntax for JS users to remember. But it will be very helpful when we'll be talking about interfaces, so let's move on! ⚑


Functions

After learning all the types and stuff above, I think it's time to finally get to know how to properly type functions! With this knowledge, you should be able to start writing some TS code for real!

Typing a function is similar to other TS code we've written before. We still have the colons and common type-name syntax but in a different place.

function myFunction(myStringArg: string, myNumberArg: number): void
{
	// code
}

As you can see, the arguments section of the function is followed by our standard type annotation. It informs the compiler about function's return value type. In the example above it's void. I mentioned earlier when talking about this special type, that it, in fact, indicates the absence of any type at all. This means that our function above doesn't return anything. Simple, right?

Naturally, there's more to typing a function than just the snippet above can show. What if we want to type a function expression, which is very common lately due to the popularity of arrow functions. So, how to do that?

const myFunctionExpression: (arg: string, arg2: number) => void =
(arg, arg2) => {
	// code
}

Above you can have a glimpse of what function type looks like. It has similar look to standard arrow functions, doesn't it?

(arg: string, arg2: number) => void

We supplied our variable with a function expression, in which our arguments aren't typed. That's because we've already done that with the function type and we don't have to repeat ourselves.

Function type, just like any other type, can also be used as argument's type for another function.

function myFunction(funcArg: () => void): void {
	// code
}

Here as an argument, I take a function which doesn't take any arguments and doesn't return anything. Again, remember that these can be easily blended with other TS types. πŸ˜‰

But what if you want to take an additional, not-required argument? How to note that something is just optional? Easy - by proceeding your argument's name with the question mark (?)!

function myFunction(myArg: number, myOptionalArg?: string): void {
	// code
}

You can have much more than just 1 optional argument. But, for obvious reasons, they cannot be followed by required arguments of any kind. There's a longer syntax for defining optional arguments, have you already thought of that?

function myFunction(myArg: number, myOptionalArg: string | undefined): void {
	// code
}

Yeah, this question mark just puts your type in union with undefined. And, as the same syntax for optional things is used in some more places, it's worth knowing that it can't be used everywhere. In such places, you can use the above syntax, and it'll always work. 😁


grayscale photography of vintage car engine
Photo by RKTKN / Unsplash

Some more functionalities

By this point (if you read the article naturally), you have a good understanding of some TS type - some basic and more complex ones. But there's much, much more to TS than just that! So, let's explore some interesting stuff that can make your TS life easier! πŸ‘


Type inference

Until now, in all previous snippets, we were strictly defining our types one by one. It almost seemed like statically-typed languages require just much more writing to be done! Don't be scared my friend - they don't! Many of these languages feature so-called type inference which allows the compiler to select the proper type for a particular variable without any special annotations. So, in places where your variables are assigned to their values just when they're declared, or when it comes to your functions' return types, you can feel free to remove your type annotation and still take advantage of all static typing goodness.

const myStringVariable = "str"; // string
const myNumberVariable = 10; // number
const myObjectVariable = {
    str: "str",
    num: 10
}; // {str: string, num: number}

As you can see, type inference makes our code look much cleaner and just better overall.

Type inference works by inferring the best common type. This means that the inferred type is just meant to be as general as possible. So, if you want to allow only e.g. strictly defined literal types, you still have to strictly annotate them.


Type guards

Remember unions? When I introduced them a while ago, did you think about how unions handle certain stuff? Because, you know, when a variable has type string, the IDE can use that information and provide you with a number of helpful things, e.g. proper autocompletion for JS string type methods. But, when the variable has type string | number then the IDE can only show you the methods that are shared between these types. What's worse, you can only assign such variable to places where it is directly specified that string | number is allowed. But what if you want to assign something like this to type string or number separately?

Well, think about it. First, you have to make sure that your variable that can have two types is exactly of the required type. How can you achieve that? With so-called type guards. And type guards is nothing more than just a fancy TS name for probably well-known to you JS operators: typeof and instanceof. In TS they have no additional functionalities over JS, so you use them just like in JS. What they do under-the-hood tho, is making your variable's type limited to a certain type - in our example it's number.

const myUnionVariable: string | number = 10;

function myFunction(arg: number) {
	// code
}

myFunction(myUnionVariable); // error
if( typeof myUnionVariable === "string" ){
	myFunction(myUnionVariable);
}

You can also easily define your own type guards by either checking if the value has a certain property, is equal to something and etc. Such type guards take a form of functions with the certain return type.

function isOne(num: number): num is 1 {
	return num === 1;
}

const myNumberVariable: number = 1; // number
isOne(myNumberVariable) // 1

Your type guard is a function that returns boolean. If it's true, your argument takes the previously annotated type. This annotation is done in the function's return type with the is keyword, having an argument's name on the left and type to convert to if the function returns true on the right. Fairly simple and straight-forward, but extremely useful when it comes to complex structures and types.


Type casting

Typecasting (also called type assertion) is an incredibly powerful technique used in many (if not all) statically-typed languages. It's useful when you know more about variable's type than the compiler does. That's an especially common case when your compilation target is a dynamically-typed language, like JS. Basically, it allows you to change the type of your variable without any restrictions, by brute-force. πŸ‘Š In TypeScript, there are 2 different syntaxes serving this purpose.

const myAnyVariable: any = "str";

const myStringVariable: string = <string>myAnyVariable;
const myStringVariable2: string = myAnyVariable as string;

You can either precede the variable you cast by angle brackets (<>) with target type inside or by using the as keyword followed by target type. There's no difference between these methods, so feel free to choose your best.

Now, in the example above I cast the variable of any type to string but this is something that you might not even stumble upon if you don't use any in your code (strongly recommended). But, believe it or not, type casting has much more use-cases. Just be sure to not overuse it, as it can drastically limit the type-safety of your code without notice.


A lot to cover

If you've read this article up to this point, then congratulation - you've made your first steps in TypeScript and the world of statically-typed languages in general. But there's still a lot more to cover! So, if you like this post, consider sharing it with others who would very much want to learn TS and leave a thumb up or any reaction below, to let me know if you want a follow-up where I'd discuss more advanced topics like interfaces, generics, and some other cool techniques! Also, follow me on Twitter and on my Facebook page to stay up-to-date with this series and a lot more interesting content about JS!

I hoped this article brought you some insights about TS and encourage you to broaden your knowledge. Again, sorry if I didn't explain everything super in-detail and technically, as this was meant to be a friendly, introductory tutorial. 😁 Anyway, thanks for reading and see you next time! πŸš€