DEV Community

Cover image for Just enough Typescript to build Projects.
Harsh
Harsh

Posted on

Just enough Typescript to build Projects.

What is the point of using TypeScript ?

JavaScript is a dynamically typed language meaning type checking is performed at runtime or at the time of execution.

JavaScript allows accessing values which aren't present leading to unexpected bugs.

const person = { name: "John Doe", id: 1 };

console.log(person.email) // <- undefined
person.name.toUppercase() // <- will throw TypeError
Enter fullscreen mode Exit fullscreen mode

TypeScript was created to solve these type of problems by adding static type checking to JavaScript.

Think of JavaScript as a person which doesn't provide feedback until you Beat the s- I mean ask it. The feedback provided however is not much in-depth such as where might the error be until you console log it like above.

TypeScript is a typed superset of JavaScript which provides static type checking (type checking is performed at compile time) to catch potential bugs before runtime. It adds type safety to your JavaScript code.

With TypeScript, The above code will now throw an error in the IDE before runtime.

TypeScript Error

Typescript is basically a more wiser version of JavaScript which helps you catch a lot of errors including typos, uncalled function (Math.random * 6), etc.

TL;DR

types + Javascript = Typescript.

Basic Types

Before using Typescript we need to know about JavaScript types and some special TypeScript types.

Here are a bunch of them -

  1. string
  2. number (there's no int or float data type in JavaScript)
  3. boolean
  4. Arrays (datatype[] or Array<datatype>)
  5. object ({})
  6. any (NOTE :- This removes the type safety provided by TS, meaning you will be back to the same old JavaScript for that particular variable)
  7. undefined
  8. void
  9. never
  10. unknown
  11. Union Types (combination of two or more types)
  12. Enum

and more.

Type Annotations and Inference

TypeScript allows you to optionally specify the type of variable while declaring it.

let name: string = "senbo";
let count: number = 0;
let isEven: boolean = true;
let array: number[] = [1, 2, 3, 4, 5];
let obj: { a : number; b : number; c?: number } = { a : 10, b : 20 
let age: number | string = 20; // union type, age is either a number or string
let alignment: "left" | "centre" | "right"; // Union of literal types
};
Enter fullscreen mode Exit fullscreen mode

These are called Type Annotations.

In the example of object, we use ; separate the properties and declare a property optional by adding ? after its name.

But this is redundant as TypeScript can automatically infer the type of variable based on assigned value.
Type Inference

This is called Type Inference.

Functions in TypeScript

While declaring a function you can add Type Annotations to parameters and return type, so that arguments to that function gets checked.

function add(a: number, b: number): number  {
  return a + b;
} 
Enter fullscreen mode Exit fullscreen mode

In the above function, a and b are assigned the type number as well as the return type which comes after the closing parenthesis of parameter list, note that the return type can be inferred by typescript so it is not necessary to write always.

Here are some more examples

function print(name: string): void {
  console.log(`Hello ${name}`);
}

function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr);
}

function printLength(item: string | string[]) {
  if (typeof item === "string") {
    console.log("Length:", item.length);
  } else {
    console.log("Lengths:", item.map(str => str.length));
  }
}
Enter fullscreen mode Exit fullscreen mode

The first function, print, simply console logs the string, hence the return type is void.
The second function, sum, uses the Rest parameters syntax, allowing it to accept an indefinite number of arguments so we use array of numbers.
You may want to revisit reduce function.
and in the last function, printlength, item is either a string or an array of string, inside the function we use typeof to check its type. If it is a string, we simply log the length of the string otherwise if it is an array, we iterate over each string in the array and log their lengths. This is called Narrowing.

Type Aliases

It is a name for a type, It is used to create reusable types or if you simply want to separate writing types for convenience.

You may capitalize type aliases name to differentiate them from variables and functions.

type Person = {
  name: string;
  age: number;
};

function greet(p: Person) {
  console.log(`Hi ${p.name}, you are ${p.age} years old`);
}

greet({ name: "John Doe", age: 100 });

// Some more examples
type ID = number; 
type Name = string;
type Age = number | string;
type NaturalNumbers = number[];

type Body = {
  height: number;
  width: number;
  location?: string;
  readonly name: string; // This property cannot be re-assigned.
}
Enter fullscreen mode Exit fullscreen mode

We can use type aliases to create function type expressions.

type func = (a: string) => void; // function type expression

function greets(name: string, callback: func) {
  callback(name);
}

greets("John Doe", function (name) {
 console.log(`Hi ${name}`);
});
Enter fullscreen mode Exit fullscreen mode

The above type func means a function with one parameter of type string and returns nothing.

Interfaces

Interfaces are another way to name an object type. They are similar to Type Aliases but have some distinctions which we'll talk about later.

Syntax

interface Book {
  title: string;
  author: string;
  readonly pageCount?: number;
}

const book: Book = {
  title: "The Great Gatsby",
  author: "F. Scott Fitzgerald",
};

interface AnotherInterface {
  name: string;
  func: (a: string) => void; // describe a function inside interface
  func2(a: string): void; // another way to describe a function
}

// This is function type, functions in JavaScript are objects which can be called, and we can also add properties to our functions.
interface MathOperation {
  (x: number, y: number): number;
}

const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;

// Its type aliases equivalent
type MathOperationType = (x: number, y: number) => number;
Enter fullscreen mode Exit fullscreen mode

Some key differences between Types and Interfaces

Types aliases do not support declaration merging while interfaces do.

Declaration merging is adding new properties to an existing interface.

for example,

interface Object {
  a: number;
}

interface Object {
  b: number;
}

function printobj(o: Object) {
  console.log(o.a);
  console.log(o.b);
}
Enter fullscreen mode Exit fullscreen mode

Both Interface and types can be extended but have different syntax.

You can read more about these distinctions here.

You can use whatever you like, I generally use interfaces for objects and types for everything else.

Generics

lets say you wanna create a function that accepts an argument and returns it.

function identity(arg: any) {
  return arg;
};

let num = identity(5);
Enter fullscreen mode Exit fullscreen mode

This works fine but the issue with this is we lose all our type definitions for the return value of the function. If we pass in an argument of type number, the function returns the same argument but with type any.

Type is any

By using generic this problem can be solved.

function identity<T>(arg: T): T {
  return arg;
}

// Two ways of consuming generic functions
let num0 = <number>identity(5); // explicitly passing the T parameter
let num = identity(5); // let the compiler infer it
Enter fullscreen mode Exit fullscreen mode

Type is number

The T in the angle bracket is called the Type Variable, you can name it whatever you like.

Generics are used to create reusable components that work with different types of data.

Let's say now we want to pass an array of different types and return it

function identity<T>(arg: T[]) {
  return arg;
}
Enter fullscreen mode Exit fullscreen mode

Notice we don't have to specify the return type as the compiler can infer it.

If we want to have a generic type for types with a length property such as string and arrays. We can use something called a constraint.

function identity<T extends { length: number }>(arg: T){
  return arg.length;
}
Enter fullscreen mode Exit fullscreen mode

In similar way we can create generic interfaces.

interface Box<T> {
  value: T;
};

type NumberBox = Box<number>;
type StringBox = Box<number>;
Enter fullscreen mode Exit fullscreen mode

Tuples

Tuples are array but with a pre-defined length and types for each index.

Syntax

type FirstTuple = [number, string];
type SecondTuple = [number, string, boolean];

const firstTuple: FirstTuple = [1, '2'];
const secondTuple: SecondTuple = [1, '2', true];
Enter fullscreen mode Exit fullscreen mode

FirstTuple is an array of fixed length 2 with index 0 being a number and index 1 being a string. if we try access element past index 1, TypeScript will throw error.

TypeScript Error Image.

never

Type when function never returns a value, it is similar to void but it is used when function throws an exception or if a function enters an infinite loop.

function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Non-null Assertion Operator (Postfix !)

It is used to tell the compiler to assume that a value is not null or undefined, even if typescript thinks it could be nullable.

function func(arg?: number) {
  console.log(arg!++);
}
Enter fullscreen mode Exit fullscreen mode

Type Assertions

Type Assertions are way to override the type inference and explicitly set the type.

For Example

const btn = document.getElementById("my_button") as HTMLButtonElement;
Enter fullscreen mode Exit fullscreen mode

TypeScript thinks that getElementById will return some kind of HTMLElement, but you can override this by using the as operator.

Thanks for reading, I tried to condense as much as examples possible and tried cover most frequently used types.

Top comments (0)