Introduction To TypeScript.

Why care about TypeScript?

One of the key factors in writing good code is, being able to make other developers understand the intent of the code we write, this kind of intent is difficult to deliver or even sometimes missing from Javascript code. For example:

function add(a + b){
    return a + b
}

If we take a look at the function above, we can understand its intent to some extent through its definition add and its body which performs an addition and returns the result, but the addition can be anything like string concatenation or numeric addition or both. What if someone who interpreted a and bas numbers and refactored the code and made what they thought was a non-breaking change, which may look something like this.

function add(a + b + c = 0){
    return a + b + c
}

If we decide to pass strings for a and b we are headed for trouble, as we will end up not getting the desired result because of the unclear intent of the code. So the intent of the code that we write matters.

Whenever there are multiple interpretations of what are the constraints, what's going on, and what was this designed for? We are kind of lagging in providing the correct intent through our code.

In the TypeSctipt world, we have something called types where we say what type of data is going to be assigned to the identifier. For example

function add(a:number, b: number): number {
    return a + b
}
add(5,6) // 11
add(5,"6") // Argument of type 'string' is not assignable to parameter of type 'number'.

The above function's intent is clear, it states that both a and b are numbers and the function return will be a number by looking at their types. This code is not only clearer as we read it, but we are alerted to any of add that deviates from what the author originally intended.

TypeScript also holds the potential to move some kinds of errors from runtime to compile time. For example:

  • potentially absent values or values with other types than defined

  • incomplete refactoring

  • Breakage around internal code contracts (eg., an argument becomes required)

It also provides a great code auditing and authoring experience. Example:

  • On hover, we can see a tooltip with the type information of the code

  • autocomplete feature for typed code

Anatomy of TypeScript

In this section, we will compile TypeScript code and learn how the TypeScript compiler(tsc) handles our program. Finally, we will look at the program's compiled output file including the type declaration file.

TypeScript provides a command line utility tsc that compiles (transpiles) TypeScript files into JavaScript. However, we also need to provide a JSON config file so that the TypeScript compiler (tsc) can look for TypeScript files in the project and generate valid output files at the correct location.

src/
index.ts
tsconfig.json
package.json

// tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist", // where to put the TS files
    "module": "CommonJS", // use CommonJS modules
    "target": "ES3" // which level of JS support to target
  },
  "include": ["src"] // which files to compile
}

When you run tsc command in a directory, the TypeScript compiler looks for the tsconfig.json file in the current directory and The directory where the tsconfig.json is located is considered the root of the project. This should compile our TypeScript code and within our project directory a ./dist folder will appear, Inside our dist folder the .js files contain vanilla javascript and the .d.ts files (declaration files) will contain only the type information related to its .js file

dist/→ index.js index.d.ts

We start with TypeScript which is JS code with typed information and then our tsc compiler generates separate files for vanilla JS .js and declaration files with only the type information .d.ts. This lets people who are just using JavaScript and who are not writing any TypeScript then can just make use of the pure JS files and people who are writing TypeScript can kind of use the declaration files to make use of types that describe the constraints.

Variable Declarations In TypeScript

Type Inference

In TypeScript, variables are “born” with their types. TypeScript is able to infer type based on the fact that we’re initialising it with a value as we are declaring it.

A type as a set of allowed values:

let age = 6
age = "irfan" // type error

In the above example, TypeScript has inferred the type of identifier name to be a number it can be any number from the set of numbers, this is called Inference, if we try to re-assign name with any type other than its inferred type number, it will throw an error.

Let’s try the same thing with const:

const age = 6
age = 7 // type error

We can notice the difference in the case of using const, type is not number It's 6. TypeScript can make a more specific assumption here as const variable declarations cannot be reassigned and inferred type is number, Therefore the type will always be set to the value 6 which is number giving any other value even if it is a number will throw an error in such a case.

Literal Type

The type 6 from the above example const age = 6 is called the literal type*.* If our let declaration is a variable that can hold any number, the const declaration can hold only a specific number 6.

Literal Type
const id = 8;
id = 8; // still error

Type Casting
let id = 8 as 8;
id = 8 // no error

Type Casting

Any Type

Sometimes we might need to declare an identifier without initialising it with any value, For Example:

let id
id = "john"
console.log(id)
id = 123
console.log(id)

In cases, where we need to declare variables before they get initialised like id above, In such case id will result in any type, since TypeScript doesn't have enough information about the declaration of id, as types are assigned to identifiers only when they are born,5 so it gets the most flexible type any.

We can think of any type as the normal variables we declare in javascript, where we can declare one data type and then reassign it to other data types like string, object, number...

Type Annotations

Variables

Up until now, we have discussed how TypeScript defines types to the identifiers in different ways, while this provides an improved safety than vanilla JavaScript If we want more control we can define types by adding type annotations.

let id: string
id = "john"
console.log(id)
id = 123
console.log(id)

Type string is annotated to the identifier id, when we try to re-assign id, it only accepts the values that satisfy the defined type when the identifier id was declared since we are re-assigning it with a number which is of a different type, TypeScript will alert us as we see below.

Functions

Type annotation works in the same way for functions as seen for variables, types are defined to the function arguments and return values. and if there are no default parameters or if no return type is defined then TypeScript will infer any type for them, Let's understand this with the below example.

function add(a, b) {0
  return a + b 
}

const result = add(4,"5")

As you can see by default everything ends up being type any, Now let's define some type annotations to the function. First, we define types to the arguments and If we want we can specifically state a return to the function as shown below, The function only accepts and returns values that satisfy with types defined during its declaration.

function add(a: number, b: number): number {
  return a + b
}
const result = add(4, 5)

The function like add with types defined provides a much better way for code authors to state their intentions up-front and TypeScript will make sure that those intentions are being followed, if not TypeScriprt will show errors where the types were defined.

Note: If a function is not returning anything then it is better to define its type as void, instead of not defining any type, which leads TypeScipt to infer any as its type.

Collections

Now that we know how to define types for variables and functions, Let's move to define types for data collections like Objects and Arrays.

Objects:

Let's understand how to define types for objects with an example.
A movie object with properties title, year, rating

const movie = {
title: "john wick",
year: 2023,
rating: 8
}
// inferred type for the above object
//{
//    title: string;
//    year: number;
//    rating: number;
//}

Now we will assign a type structure for the object while its declaration.

const movie: { title: string; year: number; rating: number } = {
title: "john wick",
year: 2023,
rating: 8
}

Excess Property Checking

The object movie will only accept the properties defined in its type no extra or no less properties will be accepted, but we can give optional properties while defining types which may or may not be assigned to the object, For example:

The below example will throw an error as the type declared doesn't contain the property genre.

const movie: { title: string; year: number; rating: number } = {
title: "john wick",
year: 2023,
rating: 8,
genre: "action" // excess property
}
// will throw an error
// Type '{ title: string; year: number; rating: number; genre: string; }' is not assignable to type '{ title: string; year: number; rating: number; }'.
// Object literal may only specify known properties, and 'genre' does not exist in type '{ title: string; year: number; rating: number; }'.(2322)
//(property) genre: string

Optional Properties
But we can assign it as an optional property with the ? operator.

// with optional property genre
const movie: { title: string; year: number; rating: number; genre?: string } = {
title: "john wick",
year: 2023,
rating: 8,
genre: "action"
}
// without optional property genre
const movie: { title: string; year: number; rating: number; genre?: string } = {
title: "john wick",
year: 2023,
rating: 8,
}

In the above examples, both are correct and TypeScript will accept both, as optional properties can be omitted during the assignment.

Index Signatures

Let's consider we want to store a collection of objects inside an object with all collections of objects having the same structure like a dictionary for example

const mobileNumbers = {
    home: { country: "+91", area:"234", number:4534523 },
    work: { country: "+91", area:"456", number:9485634 }
}

In such cases, we can define types in the following way

const mobileNumbers:{
    [key: string]: {
        country: string
        area: string
        number: number
    }
} = {
    home: { country: "+91", area:"234", number:4534523 },
    work: { country: "+91", area:"456", number:9485634 }
}

As we can see every object stored in mobileNumbers will represent the same type.

Arrays

Describing types for arrays is easy as we just have to define the type and add [] to the end to make an array type. For example, the type for an array of numbers would look like number[]

Finally, let's initialize some complex data with different type annotations for its assignment which summarizes most of the topics covered above.

let movie: {
    title: string
    year: number
    genres: string[]
    cast: {
        star: string
        director: string
        coStars: string[]
    }
    comingSoon: boolean
    preview?: s
} = {
    title: "John Wick",
    year: 2023,
    genres: ["action","thrilling"],
    cast: {
        star: "Keanu Reeves",
        director: "Chad Stahelski",
        coStars: ["Ian McShane", "Michael Nyqvist"]
    },
    comingSoon: false
}

This is how types convey the intention of the author through code and make code more secure by defining what data to be accepted.

In the upcoming TypeScript blogs, Let's make things more interesting by introducing various concepts like Union and Intersection, Interfaces and Type Aliases, and Generics. until then keep coding...