JavaScript is now one of the most used programming languages today thanks to not only its flexibility, but also as a result of the internet boom.
We access the internet using web browsers and JavaScript comes packaged with your web browser. At some point you're going to interact with JavaScript either as an end-user or a developer whether it's on your smartphone, or your smart TV, basically anything that will at some point talk to the internet.
The "WHY" behind Typescript
JavaScript isn't perfect though, in fact, it can get pretty frustrating to work with. JavaScript is a dynamically typed language. This means JavaScript will try to help you out even when you haven't asked it to.
Let's look at an example:
let numberOne = 1;
let stringOne = "One"
let sum = numberOne + stringOne;
console.log(sum)
// Output
// 1One
Intuition will tell you that this operation should not be possible. How can we add a number to a string? JavaScript will take a look at it and say "Hey human, I can help you with that", then proceed to change your number value into a string.
While there are some benefits to this kind of behavior, it more often than not causes more harm than good. JavaScript will not complain about this behavior, until it absolutely has to.
As a JavaScript developer, you've probably encountered this behavior before. For example passing in the wrong data type as a parameter to a function.
const squareMe = (number) =>{
return number * number;
}
// Let's call the function but pass in a string instead of a number
console.log(squareMe("five"))
// Output
// NaN
👋 The most common kinds of errors programmers write are type errors, where the wrong type of value was used.
Wouldn't it be nice if we could accurately determine how our code would execute without having to worry about Javascript playing judge, jury and executioner.
Typescript to the rescue
Up to this point, we've been writing dynamic code. This is referred to as dynamic typing, where the language can decide to fill in the gaps when it needs to.
Typescript offers us a way out. Rather a way to limit the dynamic nature of JavaScript by allowing us to use static typing instead.
Static typing in a nutshell, is the opposite of dynamic typing. Instead of being as vague as possible when writing our code, we try to be as explicit as possible. Let's declare a pair of variables using typescript.
let numberOne:string = "One";
let numberTwo:number = 2;
This is what typescript allows us to do. We are explicitly declaring the data types our variables can hold. If we try to re-assign the variables with values that don't match the data types specified, typescript will complain.
numberOne = 1; // Error: type "string" is not assignable to number
numberTwo ="2"; // Error: type "number" is not assignable to string
You'll get immediate feedback from typescript write in your code editor. This type checking
functionality is what has propelled typescript to be one of the most loved languages in recent years.
The graph below is Stack Overflows' Annual Developer Report 2022 for the most loved programming languages.
So Typescript in a nutshell, is still JavaScript, but a statically typed or strongly typed JavaScript.
Now let's begin to look at some of the features that are available to us when using typescript starting with the tsc
.
The tsc
TSC stands for typescript compiler. The typescript compiler is the brains behind typescript. It gives us a plethora of options to choose from when it comes to configuration options.
Before we can even interact with typescript though, we need to first install it on our machines.
npm i typescript -g
or
yarn add typescript
Confirm you have it installed by running tsc -v
on your terminal.
tsc -v
Version 4.7.4
To initialize a typescript config file, run the following command in your terminal window
tsc --init
This will generate a configuration file for you that the typescript compiler will then use to run your typescript code.
Common Configuration options
They're a lot of options to choose from, but we won't dive into the details of what they all mean. We will however look at the common configuration options that you will more often than not be interacting with.
noImplicitAny
This configuration option ensures that types are not inferred. This has the added advantage of ensuring that we always pass in the correct types when executing our program. By default, tsc will infer the any
type to your variables. This is the same behaviour vanilla javascript gives you.
With the implicitAny
option enabled, variables inferred with the any type will issue an error.
strictNullChecks
This option allows us to handle operations that return null
or undefined
more elegantly. We've all at one point or the other forgotten that a particular function doesn't return anything and tried to operate on them.
target
This option refers to the version that typescript will emit after transpiling your code to vanilla javascript. Remember your browser doesn't understand Typescript and that's why we need to specify the target version of javascript.
module
Specify the JS Module System that you'd like to use.E.g commonJS
, AMD
, ECMAScript
strict
The strict option specifies how strict tsc should be when it comes to type checking. As good practice, you should always have this option turned on.
The basics
The tsc
is responsible for checking your code before executing. Let's look at an example of valid JavaScript code, that shouldn't intuitively work, but does.
function shineMyName(firstName){
console.log(`This little ${firstName} of mine, I'm gonna let it shine..`)
}
console.log(shineMyName())
// Output
// This little undefined of mine, I'm gonna let it shine.
That doesn't look right does it? We forgot to pass in the firstName
parameter to our function, but we were still able to run our code.
If you try to run the same code in typescript, you'll get immediate feedback from the tsc
complaining that you didn't pass in all the parameters.
function shineMyName(firstName){
console.log(`This little ${firstName} of mine, I'm gonna let it shine..`)
}
console.log(shineMyName())
// Expected 1 argument but got 0 ts(2554)
// An argument for firstName was not provided
Let's add more type-checking capabilities by explicitly defining the data types.
function shineMyName(firstName:string){
console.log(`This little ${firstName} of mine, I'm gonna let it shine..`)
}
console.log(shineMyName("Bob"))
This is referred to as type annotating. Let's look at a few more example of type annotation.
let lastName: string = "Marley";
let age:number = 45;
This method of annotating is called explicit typing where we explicitly define the type of value.
Typescript also has the ability to infer types. This is called type inference
let lastName = "Marley"; // string
let age = 45; // Number
💡 When you don't explicitly annotate the types, typescript will infer the types for you. This is called implicit annotating.
Primitive types in Typescript
- String
- number
- boolean
- null
- undefined
- void
- never
- any
- symbol
- bigint
let firstName: string = "Mark";
let age:number = 30;
let isCool:boolean = true;
let bigNum:bigint = 100n;
let weDintKnow:any = "I don't know";
let maybeWeknow:unknown = "I might know";
let notDefined:undefined = undefined;
let nulling:null = null;
let never:never;
let symbol:symbol = Symbol("symbol");
let voided:void = undefined;
Defining Types in Typescript
Arrays
To specify an array in typescript, we use the syntax type[]
. You start by defining the data types that the array will include followed by a pair of square brackets.
let lyrics: string[] = ["Hey", "I", "Just", "Met", "you", "and", "this","is","crazy","but","here's","number","so","call","me","maybe"]
We first define the types of values that should be stored in our array. In our example, we can only have strings in our array.
Tuples
A tuple resembles an array but with a few differences:
- The length of the tuple is fixed,
- The tuple can have a mix of data types,
let rgb:[number,number,number] = [239,120,200]
Functions
You'll want to check the type of values being passed to our functions and define the type of value that the function should return.
We can do this by annotating our arguments as well as defining the type of value to be returned.
function lyricsMatch(lyrics:string[]):string{
return lyrics.join(' ');
}
console.log(lyricsMatch(["Hey", "I", "Just", "met", "you","," "and", "this","is","crazy","but","here's","number","so","call","me","maybe"]))
// Hey I just met you, and this is crazy but here's my number, so call me maybe
We're explicitly telling typescript, that our function will take a single argument that has to be an array of string(s). We are also indicating that our return value will be a string as well.
If we try to pass in a parameter that isn't an array of string(s), the tsc will complain.
lyricsMatch("I'm never gonna dance again")
// Argument of type string is not assignable to string[]
Union Types
A union
type is a combination of one or more types. Let's look at an example.
function returnAge(age:string|number){
return age;
}
console.log(returnAge(20))
console.log(returnAge("20"))
// The age argument can either be number or string type
// Both calls to `returnAge` will execute successfully.
Something to keep in mind though is that when you use unions, you can't use methods that are available to one of the data types only.
For example we can't do something like age.toUpperCase()
.
If the parameter that we passed in was a number, it wouldn't work. To get around this, we use narrowing.
function returnAge(age:string|number){
if(typeof age === "string"){
return age.toUpperCase()
}
return age;
}
Narrowing simply means we try to deduce the data type that was passed in. If it's of type string, then we know we can use the toUpperCase()
method on it.
Type aliases
Let's define an object shape
{
name:string,
material:string,
status?:boolean
}
Now let's assume we want to keep reusing this shape throughout our code base. We can save this blueprint using type aliases.
type glassJar = {
name:string,
material:string,
status?:boolean
}
Now we can reuse this object shape anywhere in our code base without having to keep rewriting the shape itself.
const jarOne: glassJar = {
name:"Mikes Jar",
material:"glass",
}
You can use type aliases to define any type of data structure and not just objects.
type ID = number | string;
type userIDs = string[] | number[];
Interfaces
An interface is an alternative syntax that we can use to create objects.
interface glassJar:{
name:string,
material:string,
status?:boolean
}
const jarOne: glassJar = {
name:"Mikes Jar",
material:"glass",
}
Literal Types
To explain type literals, we need to think about let
,var
and const
and how typescript interprets these values.
We know that using let
and var
allows us to change the values at a later time.
let buzz = "Buzz Light Year";
var woody = "woody";
// How typescript represents this: let woody:string
// let buzz:string`
We are telling the compiler that we want the values represented by the two variables to be any string values.
But what about declaring types with const
. These are values that we don't plan on ever-changing.
const peterParker = "Spidey";
// How typescript represents this:
// const peterParker:"spidey"
Because we can only represent a single string value, the variable is assigned the type "spidey".
Conclusion
💡The biggest advantage of using typescript is the ability to catch type related bugs at compile time.
I only scratched the surface in terms of what typescript can do but hopefully this post can push you to adopt it if you haven't already. It's a great tool that will boost the quality of the code that you write.
Thanks for reading and happy coding. 👋
Top comments (0)