Intermediate Typescript - Part 1

Intermediate Typescript - Part 1

Let's delve further into some of the typescript's intermediate principles today. I advise you to spend some time reading previous blogs. These are the concepts I learned in the Making TypeScript Stick and Intermediate TypeScript courses from Frontend Masters. I'm hoping that this will make it easier for you to learn and comprehend typescript better.

Let's get going!

Declaration Merging

A value, a type, and a namespace can all be piled on top of one another as a single entity in Typescript and exported with a single identifier. This process is known as "Declaration merging". Declaration merging, in its simplest form, is when the typescript compiler combines two or more types into a single declaration if they have the same name in a single definition. Interfaces, namespaces and enums, can all be combined, but classes cannot. In order to better comprehend, let's use an example.

In the realm of TypeScript, a lot of things, such variables and interfaces, can be declared and afterwards referenced. Here, we have an example of a Course1 interface with a business value which is a type of Course1.

image.png

TypeScript allows us to stack multiple things onto a single identifier. In this situation, we've named our interface and const variable the same thing. They both are Course2, both with the capital letter.

interface Course2 {
  name: string;
  type: "paid" | "free";
  description: string;
}

const Course2 = {
  name: "Business",
  type: "paid",
  description: "some description",
};

export { Course2 };

However, take a look at what happens if we try to expose this outside of this TypeScript module. As we can see, this tooltip includes both our interface and const declaration. The term Course2 has two items stacked on top of it.

image.png

Merging Interfaces

The members of the declared interface types are simply merged into a single interface and given a single identifier, making interfaces the most popular declaration merging type in typescript.

interface Course3 {
  name: string;
  type: "paid" | "free";
  description: string;
}

interface Course3 {
  thumbnailImgSrc: string;
}

const paintingCourse:Course3 = {
  name: "Painting",
  type: "free",
  description: "some description",
  thumbnailImgSrc:"./public/images/painting"
}

Look at how the two interfaces have been combined. When merging, the latest interface always takes higher priority. Typescript will throw an error if we do not add all members from both interfaces to the variable user.

image.png

interface Course3 {
  name: string;
  type: "paid" | "free";
  description: string;
}

interface Course3 {
  thumbnailImgSrc: string;
  seatLimit: number;
}

interface Course3 {
  seatLimit: string;
}

What if a property name appears several times in interfaces but the datatype is different? As you can see, property id has the type string in the second interface whereas it has the type number in the first interface. Typescript compiler raises an error saying, "Property 'id' must be of type 'number', but here has type 'string'".

The types of the properties must match for any interfaces that are to be combined that have a property with the same name that isn't a function; otherwise, the compiler will throw an error.

image.png

What happens if functions specified in Interfaces have the same name but distinct parameter types? The components of the merged interfaces that are functions with the same name are overloaded, which implies that the appropriate function will be called depending on the kind of input received.

interface Course3 {
  name: string;
  type: "paid" | "free";
  description: string;
}

interface Course3 {
  thumbnailImgSrc: string;
  seatLimit: number;
  displayStudentDetails(rollCall: number): number;
}

interface Course3 {
  displayStudentDetails(rollCall: string): string;
}

const paintingCourse: Course3 = {
  name: "Painting",
  type: "free",
  description: "some description",
  thumbnailImgSrc: "./public/images/painting",
  seatLimit: 100,
  displayStudentDetails: (rollCall) => {
    return rollCall;
  },
};

console.log(paintingCourse.displayStudentDetails(21));
console.log(paintingCourse.displayStudentDetails("vu4s2122016"));

Application of Declaration Merging in the real world

I needed a utility function to add one hour to the date object for one of my most recent projects.

Declarations in the global scope can also be added from within a module. For instance, we might develop the following code.

declare global {
  interface Date {
    addHours: (h: number) => Date;
  }
}

Date.prototype.addHours = function (h: number) {
  this.setHours(this.getHours() + h);
  return this;
};

In the code above, we made the TypeScript compiler aware of the global Array object and then added a addHours to its prototype. Without the declare clause, we would receive an error because the TypeScript compiler is unaware that the Array global object contains the addHours function.

Modules & import-export in typescript

For JavaScript projects, there was no standardised module format prior to 2015. Several community-based solutions as a consequence evolved. We have AMD, UMD, and CommonJS, which also was popular in the node community.

In the realm of Javascript, everything you're used to seeing with model imports and exports also applies to TypeScript. The imports & exports listed below are all supported by Typescript.

// imports
import { strawberry, raspberry } from "./berries"
import kiwi from "./kiwi" // default import
import * as React from "react" // namespace import
`
export function makeFruitSalad() {} // named export
export default class FruitBasket {} // default export
export { lemon, lime } from "./citrus"
export * from "./berries" // namespace re-export

Something that was just added to the JS language (2021) is also supported by TypeScript. JavaScript has just added the namespace re-export to their draft specification for the language for the year 2021.

export * as berries from "./berries" // namespace re-export

CommonJS

Consuming CommonJS modules that perform tasks that are incompatible with how ES Modules generally operate can occasionally be challenging.

For instance, if you are utilising a fs module, one of the essential node modules. It is importable as shown below. Alternately, we may import certain items from fs using a named import. In this manner, nearly every typical JS interrupt can be handled.

You can often translate anything like

const fs = require("fs")
into
// namespace import
import * as fs from "fs"

However, there is one situation that, if you come across it, will cause a loop: when the module's single export is not something that resembles a namespace. Let's examine this situation where we effectively have a function in our standard JS code.

A function cannot be represented by a namespace. Therefore, we will encounter an error if we attempt to consume it in this manner. We will receive an error that says if we wish to use Ecmascript, imports, and exports with this module, we have to turn a compiler flag on called esModuleInterop.

// @filename: fruits.ts
function createBanana() {
  return { name: "banana", color: "yellow", mass: 183 }
}

// equivalent to CJS `module.exports = createBanana`
export = createBanana

// @filename: smoothie.ts 
import * as createBanana from "./fruits"

It's true what this error message is saying. But it needs to tell us that there's an alternate solution to this problem, which is an import that is not aligned with the Ecmascript standard. And by doing this, we won't need to turn on the flag.

But why would I want to leave this flag off? If you need to enable features like esmodule and allowsSyntheticDefaultImports in order for your types to work, everyone who consumes your types will also need to enable those features.

As a result, we must be sure to remove these from our library code. Which means that if you're using one of such libraries, it's your decision whether you want to enable these or not. We should avoid forcing that decision on everyone who uses our code. Thankfully, there is another approach to this issue. And that's the non ecmascript import.

function createBanana() {
  return { name: "banana", color: "yellow", mass: 183 }
}

export = createBanana


// @filename: smoothie.ts

import createBanana = require("./fruits")
const banana = createBanana()

We're used to saying seeing const createbanana = require(" /fruits'), but we see import instead of const. This is what gives it its somewhat distinctive quality. It's the imports used to be this way in very old versions of TypeScript, and you can still use them.

Given that we have been faced with a couple of the less ideal options, we can either force users of our code to enable a compiler option or do this.

Importing non-TS things

Let's discuss importing items that aren't JavaScript modules.

using images

image.png

Consider a react component that requires an image file to run. Images can be imported as seen below. By default, TypeScript is unhappy with this. Because there isn't a TypeScript module with the name file.png in this environment, it says I can't find a module named file.png.

Therefore, we need a mechanism to instruct TypeScript to treat all imported png files as JavaScript modules with a string export by default. This can be achieved by placing module declarations in a file called global.d.ts.

declare module "*.png" {
  const imgUrl: string
  export default imgUrl
}

So, to put it simply, I state that there is a module that has the name "some arbitrary text followed by.png" and that its default export is a value of type "string". You'll see that the identical line that was failing before is now error-free and also please notice that the value of this IMG is of type string.

using module css

The following error might have occurred when you used the module CSS in your Nextjs or React apps.

image.png

To resolve it, we can add the below code in global.d.ts for react apps & next-env.d.ts for nextjs apps.

declare module "*.css";

Type inference with conditional types

With the help of the infer keyword, you can infer one type from another within a conditional type. The infer keyword can only be used in the condition expression of a conditional type. It can only be applied to the conditional type's true branch.

type ArrayType<T> = T extends (infer R)[] ? R: T;

// type of item1 is `number`
type item1 = ArrayType<number[]>;

// type of item1 is `{name: string}`
type item2 = ArrayType<{ name: string }>;

When item1 is built, the condition in the conditional type is true since number[] matches (infer E)[]. E is therefore inferred to be a number during this matching process. The first branch of the condition, E, is returned, which is resolved to be a number.

Because "name: string" does not match (infer E)[], the condition in the conditional type is false when item2 is formed. As a result, T, the second branch of the condition, which represents the original parameter entered, "name: string," is returned.

To further understand, let's examine another illustration:

The below shown example actually exists in TypeScript, it's called a ReturnType. We're goinng to try to make this ourselves

We have a game of toss of coins that returns heads or tails depending on the random number and I want to use these return keywords as a type to use it somewhere else.

const flipCoin = () => Math.random() > 0.5 ? "heads" : "tails"

We can do so with the help of infer keyword.

type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never

The ReturnOf will return the function return type as R type variable if the type passed in matches a function signature. If the condition is false, then the never type will be returned.

We're able to peel away the return type of function by making the most flexible function type we possibly could which takes any number of arguements of any type and returns anything. It's like any callable thing will match that type. And then we replace the any with infer R, which lets us define a new type parameter.

image.png

As you can see the type of SidesOfCoin is heads or tails.

Indexed Access types

Indexed Access types offer a way to use indices to retrieve specific elements of an array or object type. Using its property key, it takes a piece of type information from another type.

Let's say we have an interface named Course; in this case, we can simply take the type of the name attribute.

interface Course {
  description: {
    name: string;
    prerequisites: {
      courses: string[];
    };
  };
  instructor: {
    name: string;
  };
}

let courseName: Course["description"]["name"]; // string

You must use an index that is a legitimate "key" which you could attach to a Car value. What happens if you try to violate this rule is shown below:

image.png

Consequently, you'll get an error message. All of these index access types will flash to indicate an error if you try to delete something from the Course that you believe is unneeded or if you make a spelling mistake.

As long as each component of the union type is a valid index, we may also pass a union type | via Course as an index. image.png

This indexed access type returns a union type if a union type is passed through it.

Note that although we are aware that we can use . notation when working with data enclosed in square brackets. As you can see, they won't work in this case; square brackets are mandatory.

image.png

Rest parameters in typescript

We can specify an infinite number of arguments as an array in typescript by using the rest operator and the rest parameter syntax.

function getAverage(...marks: number[]) {
  let avg = marks.reduce(function (a, b) {
      return a + b;
    }, 0) 

  return avg;
}

Marks averaged using the rest parameter. The average of the numbers is output in the sample code above, which uses the rest parameter, which allows us to pass in any number of inputs.

Rest parameters can also be used in types. Additionally, we can use more than one spread in a single tuple.

type MyTuple = [
  ...[number, number],
  ...[string, string, string]
]
const x: MyTuple = [1, 2, "a", "b", "c"]

The function definition must end with the rest parameter. Otherwise, a compiler error will be displayed by TypeScript.

function getAverage(...marks: number[], studentName: string) { // Compile Error
  let avg = marks.reduce(function (a, b) {
      return a + b;
    }, 0) 

  return avg;
}

It's crucial to keep in mind that only one...rest[] element can exist in a particular tuple, however it doesn't always have to be the final one.

type YEScompile1 = [...[number, number], ...string[]]
type NOcompile1 = [...number[], ...string[]] // error

type YEScompile2 = [boolean, ...number[], string]

Awesome! We are done for the day. Thank you very much for the patience. I’d love to hear your feedback about the post. Let me know what you think about this article

Did you find this article valuable?

Support Prerana Nawar by becoming a sponsor. Any amount is appreciated!