Typescript for frontend developers

Typescript comes with a severe learning curve and no doubt for beginners.

Hey developers! I know you are working on an exciting project. I assume that, while starting the project, a person from management asked you to use Typescript and you are aware of TS. A year ago when Typescript was an immense fear for me, I never thought I will be using it in my every project and loving it. Typescript is an excellent language that has many cool features like

  • Catches errors at compile-time.

  • Provides documentation.

  • Allows editors to flag errors while typing and provides completion.

  • Makes refactoring less error-prone.

  • Makes some tests unnecessary.

  • Disallows many JavaScript-type coercions.

  • Very helpful in frontend frameworks like Vue and React, obviously in angular too

Typescript comes with a serious learning curve, and no doubt for beginners, it can give you a tough time but you have to learn it and grasp its advanced concepts because now the industry demands this technology almost in all new projects. But stay with me, because I am going to tell you some senior typescript concepts, I know you‘ll be loving them. Let’s go developers.

Remember major Types in typescript

  • "Hello" is a string

  • 2 is a number

  • true/false is a boolean

  • {} is an object

  • [] is an array

  • NaN is not a number, not basically a real type.

  • null is empty or unknown

  • undefined means no value is set

credit: Conrad Davis Jr. on medium

What is type noImplicitAny

Typescript’s default behavior is to set the default type to function, or variable and mostly it types any for variables. Here is the function;

function testijgNoImplicitAny(args) {
  return args;
}

https://res.cloudinary.com/dzonsuv6y/image/upload/v1673409571/post5/Screenshot_2023-01-11_085911_cpjz6n.png

So here typescript will infer type automatically. Typescript gives you control over types. If making a variable typed is mandatory then there is an option to tell the compiler to produce an error on untyped code, so here is how to do this

Add "noImplicitAny": true

{
  "compilerOptions": {
    "target": "esnext",
    "noImplicitAny": true    -> here is the line
}

underlined error in red color - very nice it is working

https://res.cloudinary.com/dzonsuv6y/image/upload/v1673410510/post5/Screenshot_2023-01-11_091438_qgsiax.png

To remove this error we can use any type such as any, number, string

What is type unknown

The unknown type is similar to any type in that all types are assignable to any, and unknown type.

  • Assignments to and from unknown require a type check, whereas assignments to and from any do not.

  • The unknown is a more restrictive type than any, meaning that fewer operations are allowed on values of type unknown without a type check.

  • unknown can be narrowed to a more specific type using type guards, whereas any cannot.

function example1(arg: any) {
  const a: str = arg; // no error
  const b: num = arg; // no error
}

function example2(arg: unknown) {
  const a: str = arg; // 🔴 Type 'unknown' is not assignable to type 'string'.(2322)
  const b: num = arg; // 🔴 Type 'unknown' is not assignable to type 'number'.(2322)
}

any type is bidirectional, whereas unknown is unidirectional.

Union and intersection types

An intersection type is a type that combines several types into one; a value that can be any one of several types is a union type. The & symbol is used to create an intersection, whereas the | symbol is used to represent a union.

Union types

Are joining multiple types with the "pipe" (|) symbol. For example, the type string | number is a union type that can be either a string or a number.

let value: string | number;

value = "hello";
console.log(value.toUpperCase()); // okay, value is of type 'string'

value = 42;
console.log(value.toFixed(2)); // Error: value.toFixed is not a function

Intersection types

Are joining multiple types with the "ampersand" (&) symbol. For example, the type string & number is an intersection type meaning one variable is a string and the other is a number.

interface A {
    x: number;
}

interface B {
    y: string;
}

let value: A & B;

value = { x: 1, y: "hi, ts developers" }; // okay
console.log(value.x); // 1
console.log(value.y); // "hi, ts developers"

Using Union and Intersection types together rock, for example

interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeId: number;
  salary: number;
}
interface Student {
  studentId: number;
  major: string;
}

type PersonType = Person & (Employee | Student);

let person: PersonType;
person = {
  name: "John",
  age: 25,
  employeeId: 123,
  salary: 50000
};

console.log(typeof person.name, person.name); // string John
console.log(typeof person.employeeId, person.employeeId) ; // number 123

How to use Keyof in typescript

In TypeScript, the keyof operator is used to create a new type that represents the keys of a given object type. The new type is a union of the string literals of the object's property names. To be honest, since I knew this type I am learning to use it. I mostly use it with Typescript record types.

A bit of usage of keyof

interface Person {
    name: string;
    age: number;
}

type PersonKeys = keyof Person;

let key: PersonKeys;
key = "name"; // okay
key = "age"; // okay
key = "address"; // Error: "address" is not a property of type 'PersonKeys'

PersonKeys is a union of the string literals of the property names of Person, only the values "name" and "age" are valid values for key.

Use-case of using keyof

interface Car {
    make: string;
    model: string;
    year: number;
}

const myCar: Car = {
    make: "Toyota",
    model: "Camry",
    year: 2020
};

function updateCar(
    car: Car,
    prop: keyof Car,
    value: string | number
) {
    car[prop] = value;
}

// 👍 okay
updateCar(myCar, "make", "Honda");
// 👍 okay
updateCar(myCar, "year", 2022);

// 👎 Error: "color" is not a property of type 'keyof Car'
updateCar(myCar, "color", "red");
// 👎 Error: "year" is not a property of type 'string'
updateCar(myCar, "year", "2022");

There is an interface Car that defines a car with three properties: make, model, and year. There is an object named myCar interfaced Car.

The updateCar function takes three arguments:

  • car of type Car.

  • prop of type keyof Car.

  • value of type string | number which can be string or number based on the prop.

The function modifies the value of the property identified by prop to value.

How Typeof in Typescript is different

JavaScript already has a typeof operator you can use in an expression context:

// Prints "string"
console.log(typeof "Hello world");

TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property.

let text: string;
const firstName: typeof text;
const lasrName: typeof text;

firstName, lastName are a variable whose type is inferred using the typeof operator and the value of text. Because text is of type string.

ReturnType in typescript

The ReturnType utility type in TypeScript is used to extract the return type of a function or method.

function add(a: number, b: number): number {
    return a + b;
}

let addReturnType: ReturnType<typeof add>;

add is a function that takes two numbers as arguments and returns a number.

addReturnType is a variable whose type is inferred using the ReturnType utility type and the function add. And ReturnType<typeof add> is the same as number.

Use-case of ReturnType

Here is a real-world example using array functions.

data

const data = [
    { id: 1, name: 'John' }, // id -> number, name -> string
    { id: 2, name: 'Jane' }
];

implementing some array functions

const findById = (id: number) => data.find(item => item.id === id);
const getName = (item: any) => item.name;
const toUpperCase = (str: string) => str.toUpperCase();

extracting ReturnType and storing them in variables

let foundItem: ReturnType<typeof findById> = findById(1);
console.log(foundItem); // { id: 1, name: 'John' } // { id: number, name: string }

let itemName: ReturnType<typeof getName> = getName(foundItem);
console.log(itemName); // 'John'  // string

let upperCasedName: ReturnType<typeof toUpperCase> = toUpperCase(itemName);
console.log(upperCasedName); // 'JOHN' // string

What are Generics in Typescript

As per typescript docs,

Being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

When you create a generic component, you can specify a placeholder type (also called a type parameter) that represents the actual type that the component will work with. Mostly this parameter is set to T like <T> and these angle brackets are known as type-interface.

Syntax of Generics

A simple generic function that takes an array of elements and returns the first element of the array:

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

T is the type parameter that represents the actual type of elements in the array. When you call the syntax function, you can specify the type of elements by providing a type argument. So in simple words, arguments will decide the final type.

function getFirst<T>(arr: T[]): T {
    return arr[0];
}

let numbers = [1, 2, 3];
let firstNumber = getFirst<number>(numbers); // 1

let strings = ['a', 'b', 'c'];
let firstString = getFirst<string>(strings); // 'a'

type of the elements in the array is inferred from the array argument.

Using Conditional types in Typescript

The conditional ternary operator is a very well-known operator in Javascript. The ternary operator takes three operands. A condition, a return type if the condition is true, and a return type is false.

syntax

condition ? returnTypeIfTrue : returnTypeIfFalse;

Before going forward a bit of note about extending types

<aside> 💡 In TypeScript, you can use the extends keyword to create new types that are based on existing types.

interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    salary: number;
}

Here is how to use conditional types by leveraging generics <T>.

interface IdWithString {
  id: string;
}

interface IdWithNumber {
  id: number;
}

type Id<T> = T extends string ? IdWithString : IdWithNumber;

let idA: Id<string> = { id: "stringId" };
// Type is inferred as IdWithString

let idB: Id<number> = { id: 1 };
// Type is inferred as IdWithNumber

The Id type is a generic type that takes a type parameter T, and it uses a conditional type to check if T is assignable to string

Using typeof, and keyof types together

It is rocking to use keyof and typeof together in TypeScript. The keyof keyword allows you to extract the keys of an object type, while typeof allows you to extract the type of a variable.

so here is how

const colors = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff',
}; // 1

type Colors = keyof typeof colors; // 2, note

let selectedColor: Colors = 'green'; // 3

let anotherSelection: Colors = 'yellow'; // 4

console.log(colors[selectedColor]); // 5

// docs 
/**
1. Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'
2. trying to get types of properties
3. yeah, it is correct 
4. Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'
5. '#00ff00'

note: keyof typeof colors is equal to "red" | "green" | "blue" 
*/

How the TypeScript Record Type Works

The Record type is used to create an object type that has a set of properties with a specific key type and a specific value type. For a beginner, it is a bit tough to understand types per data type.

When I was young in typescript I usually use an empty object {} as a type of object, then IDE told me, dude! there is a Record type, use that.

Syntax of Record type

Record<K extends keyof any, T>

Where K is the type of the keys of the object and T is the type of the values of the object.

type TRecords = Record<string, number>;

let tryingRecords: TRecords = {
    "key1": 1,
    "key2": 2,
};

tryingRecords is an object type that has properties with keys of type string and values of type number.

Using typescript Records and union types together

A practical example of how you can use Record and union types together in TypeScript:

interface User {
    id: number;
    name: string;
}

interface Admin {
    id: number;
    name: string;
    permissions: string[];
}

interface Guest {
    id: number;
    name: string;
    expiration: string;
}

type UserType = "user" | "admin" | "guest";

type Users = Record<UserType, User | Admin | Guest>;

let users: Users = {
    user: { id: 1, name: "John" },
    admin: { id: 2, name: "Jane", permissions: ["read", "write"] },
    guest: { id: 3, name: "Jim", expiration: "2022-12-31" }
};
  1. There are three interfaces that represent different types of users (User, Admin, and Guest),

  2. There is a union type UserType to combine three types into a single type that can be one of "user", "admin" or "guest".

  3. There is an object with Record type to define a new type Users. Type Users is an object type with properties that have keys that can only be the string literals "user", "admin", and "guest",

  4. values of type User | Admin | Guest which is a union of all the three interfaces.

Using record types with typeof, and keyof without interfaces

But there is a better way to type objects without even the use of interfaces, and I name it time-saving types.

data

// Data
const productInventory = {
    shirts: {
        small: 10,
        medium: 15,
        large: 20
    },
    pants: {
        small: 5,
        medium: 10,
        large: 15
    },
    shoes: {
        size7: 25,
        size8: 30,
        size9: 35
    }
};

logic


type Product = keyof typeof productInventory;
type ProductSizes = typeof productInventory[Product];

function checkInventory(product: Product, size: keyof ProductSizes): boolean {
    return productInventory[product][size] > 0;
}

console.log(checkInventory("shirts", "medium")); // true
console.log(checkInventory("pants", "small")); // true
console.log(checkInventory("shoes", "size9")); // true
console.log(checkInventory("pants", "extra large")); //compile error, extra large is not key of ProductSizes
  1. productInventory → information about different products and the sizes available for each product, Record type to define a new type ProductSizes

  2. ProductSizes is an object type with properties that have keys of the

  3. productInventory[Product] object and values of type number.

  4. Product is the keyof productInventory to reference the specific product.

  5. ProductSizes is the type of productInventory[Product] to reference the specific product sizes.

Closing words

For front-end developers, TypeScript can provide several benefits to improve the development experience. TypeScript can help catch errors early by providing better type safety. which can make your code more robust and maintainable.

Using TypeScript in frontend development can also help you take advantage of modern JavaScript features such as JSX, async/await, and destructuring, while still providing the benefits of static type checking. This can make it easier to write and maintain complex and large-scale applications.