šŸ”„Advanced Typescript

Learn how to use important advanced concepts in typescript with examples

šŸ”„Advanced Typescript
Advanced Typescript

Typescript is a language built on top of Javascript that was developed to allow developers to work with types in Javascript, which originally was developed as a loosely language - without any types.

Today we will go over the presentation given by Bruno Francisco in a meetup organized in codotto.com.

šŸ¦
Would you like to start going to an IT meetup or even organize one in your city? Check out codotto.com, where we make it easy for you to organize meetups, invite speakers and get in touch with the members of the community

What is typescript

Typescript is a programming language that is a superset of Javascript. It was developed and maintained by Microsoft. Typescript adds optional static typing and class-based object-oriented programming to Javascript. This allows for improved code readability and reliability. Because Typescript is a superset of Javascript, any existing Javascript code can be easily incorporated into a Typescript project. In addition, Typescript code can be compiled into plain Javascript that can be run on any platform that supports Javascript. Overall, Typescript provides a way to write scalable and maintainable Javascript code.

Typescript vs Javascript

Typescript and Javascript are both programming languages that are used for web development. Javascript is the most popular programming language for web development, and it is supported by all modern web browsers. Typescript is a superset of Javascript, which means that any valid JavaScript code is also valid Typescript code. However, Typescript adds additional features to JavaScript that make it easier to write large and complex programs.

One of the key differences between Typescript and Javascript is that Typescript is a typed language, while Javascript is a dynamically typed language. This means that in Typescript, variables and functions must be declared with a specific type, such as number or string. In contrast, in Javascript, the type of a variable or function can change at runtime. This makes Typescript a more reliable and predictable language than Javascript.

Another major difference between Typescript and Javascript is that Typescript supports object-oriented programming concepts, such as classes and interfaces, while JavaScript does not - in the ES6 spec, it is possible to use class natively without the need of Typescript. This means that in Typescript, it is possible to define classes and create objects from those classes, whereas in Javascript, objects are defined and created using a different syntax. This makes Typescript a better choice for large and complex applications that require object-oriented programming.

Overall, Typescript and Javascript are both powerful languages for web development. However, Typescript offers additional features that make it better suited for large and complex projects. It is also easier to maintain and scale Typescript code, thanks to its static typing and object-oriented programming features.

// Pure Javascript
function getFullName(user) {
  return `${user.firstName} ${user.lastName}`;
}

// Typescript
interface IUser {
  firstName: string;
  lastName: string;
}

function getFullName(user: IUser) {
  return `${user.firstName} ${user.lastName}`;
}
Javascript vs typescript differences

The important concepts we will be looking at

There are multiple advanced concepts that are used in Typescript. Unfortunately, we won't be looking at all of them today - maybe in a different talk in the subject - but the ones that we will be looking at are the ones that will most likely help you on your day-to-day life as a developer.

These are the concepts we will be looking at:

  1. Utility types
  2. Generic types
  3. "in operator"
  4. Assert functions
  5. Conditional types
  6. Guard operators
  7. extends keyword

1. Utility types

In TypeScript, "utility types" are types that are defined in the standard Typescript library and can be used to perform common operations on other types. These types are not intended to be used directly, but rather to be used as a building block to create more complex types.

Utility types are often used to create new types by combining or modifying existing types. For example, the Pick utility type allows you to create a new type by selecting a specific set of properties from an existing type. The Exclude utility type allows you to create a new type by excluding specific properties from an existing type.

Here is an example of using the Pick utility type to create a new type:

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

// PersonInfo is a new type with only the 'name' and 'city' properties from the Person type
type PersonInfo = Pick<Person, 'name' | 'city'>;

In this example, the Person type is defined with three properties: name, age, and city. Then, the Pick utility type is used to create a new type called PersonInfo, which includes only the name and city properties from the Person type.

Utility types can be a valuable tool in Typescript, as they can help you create new and more specific types by combining or modifying existing types. This can make your code more readable and maintainable, as well as more flexible and reusable.

Where we use utility types in Codotto

In codotto we define our types that come from the backend in interfaces. Here is an example of how we define an interface for UserTalk

import type { CreatedAt, RecordID } from '@/services/api';
import type { UserCodec } from '@/services/api/resources/users/codec';

export interface UserTalkCodec {
  id: RecordID;
  title: string;
  description: string;
  duration: number;
  presentationUrl: string | null;
  createdAt: CreatedAt;
  user: UserCodec;
  feedbackInfo?: {
    count: number;
    average: number;
  };
}

We also define what we should send to the backend whenever we would like to create a resource - in this example, whenever we want to create a UserTalk

export type UserTalkCreatable = Pick<
  UserTalkCodec,
  'title' | 'description' | 'presentationUrl'
>;

Notice how we make it clear that whenever we want to create a UserTalk, we need to send fields that are dependent on UserTalk interface. Imagine that we would add a new field called intendedAudience to our UserTalk. As soon as we add it to the UserTalk interface we would need to update our code base to send this new field intendedAudience to our backend as well

export interface UserTalkCodec {
  // ...
  intendedAudience: 'junior' | 'middle' | 'senior' | 'manager';
}

2. Generic types

In TypeScript, generic types are types that are not specific to any one particular data type. Instead, they can be used with a variety of data types. This allows for greater flexibility and reusability in your code.

For example, consider the following function that takes an array of elements and returns the first element in the array:

function firstElement<T>(elements: T[]): T {
  return elements[0];
}

In this function, the generic type T is used for the elements in the array, as well as the return type of the function. This means that T can be any data type, such as a number, string, or object. To use this function with a specific data type, you can specify the type when calling the function, like this:

const numbers = [1, 2, 3];
const firstNumber = firstElement<number>(numbers);

const strings = ['hello', 'world'];
const firstString = firstElement<string>(strings);

In this example, the firstElement function is called twice, once with the numbers array and once with the strings array. In each case, the generic type T is replaced with the specific type that is being used (number and string, respectively). This allows the function to operate on the array elements of the specified type and return a value of the same type.

Overall, generic types in TypeScript provide a way to write flexible and reusable code that can be used with multiple data types. This can help you avoid repeating the same code for different data types and make your code easier to maintain.

Where we use generics in Codotto

Our backend in codotto is using Laravel and Laravel has a pretty standard way of returning paginated results. Whenever we fetch the list of groups we get a response that looks like this

{
  "data": [
    {
      "id": "c562a066-bde6-4d09-ae65-cabbd64c15ee",
      "title": "test title",
      "description": "I want to see the meetup room !<br>",
      "slug": "test-title",
      "image": "https://api-devotto-prod-files.s3.eu-central-1.amazonaws.com/groups/covers/4d46cd6f-756c-46e9-87c2-39a4b3b03c54",
      // ...
    }
  ],
  "links": {
    "first": "https://api.codotto.com/groups?page=1",
    "last": "https://api.codotto.com/groups?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    // ...
  }
}

The links and meta will always look the same - regarding to types. The links.first, links.last will always be a string while links.prev and links.next can either be a string or nullable. The only part that will be different is the data key. This data key can contain multiple types data inside of it at a time. Sometimes we can have meetups, sometimes we can have groups or users. This is a perfect example to define a generic. We can create a PaginatableRecord interface that receives a generic whenever we want to use it

export interface PaginatableRecord<T> {
  data: T[];
  links: Links;
  meta: Meta;
}

Then we can use this PaginatableRecord as such:

const listCities: PaginatableRecord<CityCodec> = () => { ... };
const listGroups: PaginatableRecord<GroupCodec> = () => { ... };
const listMeetups: PaginatableRecord<MeetupCodec> = () => { ... };

3. "in" operator

In TypeScript, the "in operator" is a binary operator that is used to determine whether a property with a given name exists in an object. This operator returns true if the property exists, and false if it does not.

The "in operator" is typically used in a conditional statement, such as an if statement, to check whether a property exists in an object before accessing or modifying its value. Here is an example of using the "in operator" in a conditional statement:

const person = {
  name: 'John Doe',
  age: 35,
  eyeColor: 'blue',
};

if ('name' in person) {
  console.log(`The person's name is ${person.name}.`);
}

In this example, the "in operator" is used to check whether the name property exists in the person object. If it does, the code inside the if statement is executed, and the person's name is logged to the console. If the name property does not exist, the code inside the if statement is skipped.

The "in operator" can also be used with arrays to check whether an index exists in the array. For example, the following code uses the "in operator" to check whether the index 2 exists in the numbers array:

const numbers = [1, 2, 3, 4, 5];

if (2 in numbers) {
  console.log(`The number at index 2 is ${numbers[1]}.`);
}

Overall, the "in operator" in TypeScript is a useful tool for checking whether a property or index exists in an object or array before accessing or modifying its value. This can help you avoid runtime errors and make your code more reliable and predictable.

Where do we use "in operator" in Codotto

Currently we do not have any real usage of the "in operator" in our codebase but a good example would be the case where we have a codec for Meetup and another for MeetupOwner, where one brings more information about the meetup if the logged user is the owner of the meetup

interface MeetupCodec {
  id: ResourceID;
  title: string;
}

interface MeetupOwner extends BaseMeetup {
  numberConfirmedAttendees: number;
  checkInConfirmationCode: string;
}

Then, we can use the "in operator" to check if the Meetup being returned from the backend is a MeetupOwner type:

export function isMeetupOwner(meetup: Meetup | MeetupOwner): Meetup | MeetupOwner {
  return 'numberConfirmedAttendees' in meetup;
}

4. Assert functions

In TypeScript, "assert functions" are functions that are used to test the type of a value at runtime. These functions are typically used in unit tests to ensure that a value has the expected type.

An assertion function specifies, in its signature, the type predicate to evaluate. For instance, the following function ensures a given value be a string:

function isString(data: unknown): asserts data is string {
  if (typeof data !== "string") {
    throw new Error("Value should be a string");
  }
}

Where do we use "assert functions" in Codotto

In our Q&A sections, you can react to questions made to a speaker in a meetup or to the answers. The main difference is we removed the "thumbs down" emoji from the reactions. We have the following enumeration to represent the "reactions" in our application

export enum ReactionType {
  THUMBS_UP = 'thumbs_up',
  THUMBS_DOWN = 'thumbs_down',
  SMILE = 'smile',
  TADA = 'tada',
  THINKING_FACE = 'thinking_face',
  HEART = 'heart',
  ROCKET = 'rocket',
  EYES = 'eyes',
}

Whenever we update a reaction - create or delete - for Q&A section we want to make sure that we are only sending all reactions that are not thumbs_down. For that we have an assert function

function assertsIsPositiveReaction(
  reaction: ReactionType
): asserts reaction is ReactionType.THUMBS_UP | ReactionType.SMILE | ReactionType.TADA | ReactionType.THINKING_FACE | ReactionType.HEART | ReactionType.ROCKET | ReactionType.EYES {
  if (reaction === ReactionType.THUMBS_DOWN) {
    throw new Error('Reaction should not be thumbs_down');
  }
}

5. Conditional types

In TypeScript, conditional types are a feature that allows you to create new types based on the evaluation of a condition at compile time. This can be useful for defining more flexible and reusable types that can adapt to different conditions.

Conditional types are defined using the T extends U ? X : Y syntax, where T is a type parameter, U is a type constraint, X is the type that is chosen if the condition is true, and Y is the type that is chosen if the condition is false. Here is an example of using a conditional type

type IsString<T> = T extends string ? T : never;

In this example, the IsString type is defined using a conditional type. It takes a type parameter T and defines a type constraint that requires T to be a string type. If the T type satisfies this constraint (i.e., if it is a string), the IsString type is the same as the T type. Otherwise, the IsString type is the never type.

To use the IsString type, you can specify a type argument when calling the type. For example, the following code uses the IsString type to define the name variable

const name: IsString<string> = 'John Doe';

In this example, the IsString type is called with the string type as the type argument. Since the string type satisfies the type constraint of the IsString type, the name variable is typed as the string type.

Conditional types can be a powerful tool in TypeScript, as they allow you to create types that can adapt to different conditions. This can make your code more flexible and reusable, and can help you avoid repeating the same type definitions for different scenarios.

Where do we use conditional types in Codotto

Currently we do not have any implementations of conditional types in codotto.

6. Guard operators

In TypeScript, "guard operators" are a set of operators that are used to narrow the type of a value based on certain conditions. These operators allow you to ensure that a value has the expected type, and to handle the value differently depending on its type.

There are several guard operators in Typescript, including the typeof operator, the instanceof operator, and the in operator. These operators can be used in combination with type guards, which are expressions that evaluate to a boolean value and narrow the type of a value based on the result of the evaluation.

Here is an example of using the typeof operator and a type guard to narrow the type of a value

function getLength(value: unknown): number {
  if (typeof value === 'string') {
    return value.length;
  }
  return 0;
}

In this example, the getLength() function takes a value of type unknown as an argument and returns its length if the value is a string, or 0 if it is not. The typeof operator is used in the type guard to check whether the value is a string, and if it is, the value.length property is accessed. Since the type guard narrows the type of the value to string.

Where do we use guards operators in codotto

In codotto we have a component for choosing the image that you would like to crop. This component is used in updating your profile picture, updating a cover image to your meetup and many other places.

This component has a prop image that can either be Blob or string - it will be a Blob if we are using the component to upload an image to a server or string if we are receiving the image as a link from the backend.

The underlying plugin to actually crop the image only accepts URLs and not Blob. So we can use a guard operator to help us transform a Blob into a base64 representation or just return the URL in case it is a string

const imageUrl = computed(() => {
    if (!props.image) return '';

    if (process.env.CLIENT) {
    	if (props.image instanceof Blob) {
    		return URL.createObjectURL(props.image);
    	}
    }

    return props.image;
});

7. extends keyword

In TypeScript, the extends keyword is used to create a derived class from an existing class. This allows you to create a new class that inherits the properties and methods of the existing class, and to add or override members to create a custom implementation.

Here is an example of using the extends keyword to create a derived class

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  move(distance: number) {
    console.log(`${this.name} moved ${distance} meters.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log(`${this.name} barked.`);
  }
}

In this example, the Animal class is defined with a name property and a move() method. The Dog class is then defined as a derived class of Animal using the extends keyword. The Dog class inherits the name property and the move() method from the Animal class, and it adds a new bark() method.

To use the Dog class, you can create an instance of the class and call its methods, like this

const dog = new Dog('Fido');
dog.move(10);  // logs "Fido moved 10 meters."
dog.bark();  // logs "Fido barked."

In this example, an instance of the Dog class is created and assigned to the dog variable. The move() and bark() methods are then called on the dog instance. Since the Dog class extends the Animal.

The extends keyword is usually used in classes but it can also be extended to objects. We can safeguard that a parameter being passed to a function extends from a subset of type

interface Group {
  title: string;
}

interface User {
  firstName: string;
  lastName: string;
}

function getTitle<T extends {title: string}>(value: T): string {
  return value.title;
}

const group: Group = { title: 'Javascript Miami' };
const user: User = { firstName: 'John', lastName: 'Doe' };

const groupTitle = getTitle(group);
// This will give an error since `user` doesn't have property `title`
const user = getTitle(user);

Where do we use extends keyword in Codotto

In codotto you can react to many resources, such as meetup messages and questions made to speakers during the meetup. These resources that can be "reactable" are received from the backend with two properties that are well known in the frontend: reactionsGroupedBy and loggedUserReactions.

reactionsGroupedBy tells us which reactions this resource has and how many reactions. The loggedUserReactions tells us which reactions the current logged user have reacted to this resource. A concrete of example can be

{
  title: "Javascript Miami",
  reactionsGroupedBy: [
    { reactionType: 'heart', totalReactions: 30 },
    { reactionType: 'rocket', totalReactions: 1 },
  ],
  loggedUserReactions: ['heart', 'rocket']
}

We have a service that will update this object reactionsGroupedBy and loggedUserReactions properties. This service should receive an object that contains these two properties - it doesn't matter to the service if it is a GroupCodec or MeetupMessageCodec. For this we can define a generic that should extend an object that has these two properties - reactionsGroupedBy and loggedUserReactions

export interface ReactionsGroupedBy {
  totalReactions: number;
  reactionType: ReactionType;
}

export const updateReactions = <T extends { reactionsGroupedBy: ReactionsGroupedBy[]; loggedUserReactions?: ReactionType[] }>(
  reactable: T,
  reaction: ReactionType
): T => {
  // Logic to update the reactions of the object
}

Conclusion

In conclusion, Typescript is a statically-typed programming language that is built on top of Javascript. It adds support for advanced features such as interfaces, classes, and modules, as well as strong typing and type checking. These features can help developers write more reliable and maintainable code, and can improve the overall quality of their projects. Additionally, Typescript code can be easily transpiled to Javascript, allowing it to run on any platform that supports Javascript. Overall, Typescript is a powerful and versatile language that is well-suited for building complex and scalable applications.

Typescript is widely used in industry, and it is supported by many popular frameworks and libraries, such as Angular, React, and Node.js. It has a growing community of users and contributors, and it is regularly updated with new features and improvements.

The extends keyword, utility types, generic types, "in operator", assert functions, conditional types, and guard operators are all important features of Typescript. These features provide a powerful and flexible type system that can help you write high-quality and maintainable code.

The extends keyword is used to create a derived class from an existing class, allowing you to inherit its properties and methods and to add or override members to create a custom implementation. Utility types are types that are defined in the standard Typescript library and can be used to perform common operations on other types. Generic types are types that are not specific to any one particular data type, allowing for greater flexibility and reusability in your code.

The "in operator" is a binary operator that is used to determine whether a property with a given name exists in an object. Assert functions are functions that are used to test the type of a value at runtime, and can be used in unit tests to ensure that a value has the expected type. Conditional types are a feature that allows you to create new types based on the evaluation of a condition at compile time, allowing for more flexible and reusable types. Guard operators are a set of operators that are used to narrow the type of a value based on certain conditions, allowing you to ensure that a value has the expected type and to handle it differently depending on its type.

Overall, these features of Typescript provide a rich and powerful type system that can help you write high-quality and maintainable code. They can be a valuable tool for any developer who wants to take advantage of the benefits of static type checking in their Javascript projects.