Typescript, in the recent years, has become the default language of frontend projects (everything that was previously written in js). Most polls and surveys done in a frontend context, show overwhelming usage of the strongly typed language instead of using javascript directly. Though most developers like the concept of strong and static typing in theory, it is most often not easy to utilise fully.
Even when using typescript, it is easy to not get any benifits out of it at all, with missing type definitions (relying on implicit any) or definiting the types as
any. Such workarounds to bypass the correct type definitions calls into question the decision to use typescript at all. Given some of the drawbacks of typescript, some teams have stopped using it (
Turbo 8 dropped TypeScript). Not using typescript is a better decision than using it with with workarounds that take away all its advantages.
Here are some points that can be taken into consideration for weighing in on whether or not to use typescript
Pros
1. Static Typing
String static typing provided by typescript allows build time validation of errors, leading to much enhanced code quality and maintainability.
2. Readability
With type annotations, much of the documentation is covered in the type definition itself, leading to more understandable code.
3. IDE support
The IDEs have good support of typescript to allow type hints, auto complete, etc. helper tools, which make it easier to write bug free code.
Cons
1. Learning curve
One of the biggest advantages of javascript is that it is very easy to code in, but much of this is taken away by typescript, because the language needs an understanding and adoption of static typing.
2. Overhead
Typescript has deveveloper time overhead, having to spend time in defining the types. It also has significant build time overhead. Typically compiling typescript into javascript and the checks associated with the types is quite expensive.
3. Integration Challenges
The syntax and the tooling associated with compiling typescript has its own configurations and quirks, leading to potential support and integration challenges with all the other technologies associated with frontend build and deploy mechanisms.
Should you use typescript?
It depends on the goals of the project (a very small one not meant for production may not need it), but in most cases, the answer is yes. This is very similar to the answer to the question "should you write tests for your code". The most important aspect of both writing tests and using typescript is that it is an investment. Like all investments, it takes effort and does not pay off immediately. But in the long term their advantages outweigh what it costs while they are being implemented.
Sure, it will be a bit hard to learn for the team, take more time to write and build. But the it all goes into making the code more readable, maintainable, easier for the next person to make changes to it. In the long run, will prevent production outages.
Navigating the learning curve for Typescipt is a challenge and
the extensive documentation, though very thorough, does not make it easier to learn. It is hard to appreciate the advantages of typescript when the code is being written, and much like writing tests, the benifits show up much later, when the code is being maintained or enhanced. So when the objective is to quickly learn the tools of the trade so that code can be written quickly without compomising the quality of the output or reading all the documentation, the following topics should enable a good level of expertise.
Deep dive
Static and strong typing
Typescript compiles into javascript. This part is enabled by the TypeScript Compiler (TSC) and is pluggable into all common frontend bundlers, task runners and tooling solutions. The primary purpose of writing Typescript instead of Javascript is to be able to get the advantages of strong and static typing.
The TSC runs type checks on the code while compiling it, so that makes it a staticly typed language. Further, the language also is strict in dealing with types, in matters of enforcement of firm restrictions on mixing different data types and values, making it a strongly typed language as well.
Using types
Derived / Inferred types
When a value is assigned during declaration of a variable, typescript assumes the type. Strict typing applies after the definition.
let value = 10;
value = '10'; // Type string is not assignable to type number
Explicit types by annotation
Types can be explicitly assinged
let value: string;
value = 10; // Type number is not assignable to type string
value = '10';
Implicit any
When the type of a variable is not defined and its type cannot be inferred, Typescipt consideres them to be of the any type
let value; // Inferred as any
value = 10; // No error
value = '10'; // No error
Type annotations
Typescript provides mechanism to define type annotations for all javascript values
The primitive types
The most commonly used JS primitves are string, number and boolean. Each one of them have their corresponding type annotations.
let str: string;
let num: number;
let bool: boolean;
The Array type
The square braces are used to indicate an array type. The type of content contained in the array preceeds the brackets.
let strArray: string[];
let numArray: number[];
The function type
Function types are defined by the round brackets and fat arrow
let func: (arg: string) => number
This indicates that the function assignable to func has to be a function that takes in one argument of type string and returns back a number
Function annotations
Function definitions have annotations of their own. The parameters and the return type can be annotated
function myFunc(arg: string): number {
return parseInt(arg);
}
The object type
The most common type definitions are about objects. The object type definition declares the type of the object properties. An object type is defined very similar to a regular JS object, only with type declarations.
const obj: { prop1: number; prop2: string; }
Types Alias and Interfaces
The types can be defined in a couple of ways
Type Alias
The type keyword can be used to assign a type definition. This is usually used to give a name to a complex type definition, in most cases.
type Coord = {
x: number;
y: number;
};
type ID = number | string;
Interfaces
An interface declaration is another way to name an object type:
interface Coord {
x: number;
y: number;
}
Optional properties
The object type definition can also specify that properties are options. Use ? to indicate this.
interface Name {
first: string;
last?: string;
};
function printName(obj: Name) {
// ...
}
// Both OK
printName({ first: "Rahul" });
printName({ first: "Jon", last: "Doe" });
Intersection and Union types
Complex types can be built out of the primitive types. The intersection and union type operators help in doing this.
Intersection
An object type may be defined to contain all the properties of several object types. Objects qualifying to be of that type must be an intersection of the two types.
operator.
interface Color {
color: string;
transparency: string;
}
interface Cloth {
material: string;
}
type CurtainDetails = Color & Cloth;
The CurtainDetails type must contain the properties of both Colorand Cloth
Union
Another complex type definition is one that represents more than one type
type ID = string | number
To be able to determine what type the value actually is, checks for type can be done at run time and this narrows down the type of the value in that block.
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
Discriminated unions
With the above example, we see that the typeof operator can be used to narrow down the type of a union type. However, how to do that for object types? structures.
let obj: { type: 'circle | 'square'; radius?: number; side?: number }
Here both the radius and the side properties have to be optional, because the object may be a circle or square. The missing part is that both cannot be missing and one of these properties correspond to one specific type.
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
side: number;
}
function getArea(item: Circle | Square) {
if(item.kind === 'circle') {
// Item narrowed down to Circle
// item.side will not be allowed by typescript here
return Math.PI * Math.pow(item.radius, 2)
} else if (item.kind === 'square') {
// Item narrowed down to Square
// item.radius will not be allowed by typescript here
return Math.pow(item.side, 2)
}
}
Enums
Enums are special typescript constructs that define both a type and a JS object. So this is one of the few items that compile into a JS output along with being a type defintion.
// Enums can have mixed values of any type and can be computed
enum Direction {
Up = "UP",
Down = someValue(),
Left = 2,
Right = "RIGHT",
}
const direction: Direction = Direction.Up;
Because of the dual nature of Enums and how they are used, it is recommended to use objects with as constwhich can be used an an enum.
Objects vs Enums
In modern TypeScript, you may not need an enum when an object with as const could suffice:
const Direction = {
Up,
Down,
Left,
Right,
} as const;
const direction: keyof typeof Direction = Direction.Up;
Pick
This is used to define an object type with only certain properties of another type.
interface Description {
title: string;
description: string;
completed: boolean;
}
type StaticDescription = Pick<Todo, "title" | "description">;
Omit
This can be used to define an object type with some properties omitted from another type.
interface Description {
title: string;
description: string;
completed: boolean;
}
type StaticDescription = Omit<Todo, "completed">;
ReturnType
Constructs a type consisting of the return type of function Type.
type T = ReturnType<() => string>; // type T = string
keyof type operator
The keyof operator takes an object type and produces a string or numeric literal union of its keys.
type Point = { x: number; y: number };
type P = keyof Point; // type P = 'x' | 'y'
typeof
TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property:
let s = "hello";
let n: typeof s;
This can be useful in combination with other types
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>; // type P = { x: number; y: number }
const c = {a: 10, b: 11};
type Q = keyof typeof c // type Q = 'a' | 'b'
Partial
Constructs a type with all properties of Type set to optional.
interface Data {
title: string;
description: string;
}
function updateData(data: Data, update: Partial<Data>) {
return { ...data, ...update };
}
Type predicates
The section about Discriminated Union explained how to use a discriminator property and checks to narrow down on the exact of a union type value. This can also be done without using a discriminator and a more flexible run time check.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet: Fish | Bird = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Generics
Type generics are very useful for defining the type for such functions and classes that are consumed in differnt scenarios and those scenarios can only be defined by te consumer of the function or class.
function stringify(arg: any) {
return arg.toString();
}
// instead of using any, this function can use generics
function print<T extends { toString: () => string }>(arg: T) {
return arg.toString();
}
Note here that the extends keyword specifics the constraints withing which the genric type
T must fall. With the genric, the consumer can now specify the type of the argument, and use the function in a type safe way, even when at the time of definition of the function, we dont really know the type of the argument. Read more this topic in the
typescript documentation for Generics.
tsconfig
The
tsconfig.json file contains all the configuration that typescript compiler
tsc needs. Some important configurations are
compilerOptions.noImplicitAny ,
compilerOptions.baseUrl,
compilerOptions.module ,
compilerOptions.moduleResolution ,
compilerOptions.rootDir ,
compilerOptions.rootDirs ,
compilerOptions.typeRoots ,
compilerOptions.types ,
compilerOptions.outFile ,
compilerOptions.outDir ,
compilerOptions.noEmit ,
compilerOptions.declarationMap ,
compilerOptions.declarationDir ,
compilerOptions.declaration,
compilerOptions.skipLibCheck,
compilerOptions.esModuleInterop,
compilerOptions.forceConsistentCasingInFileNames,
compilerOptions.allowJs,
compilerOptions.sourceMap,
compilerOptions.removeComments