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! š
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
orfalse
; - 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.
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. š
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! š
If you need
Custom Web App
I can help you get your next project, from idea to reality.