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 b
as 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 number
s 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...