🔥 Mastering TypeScript

·

45 min read

Table of contents

Typescript is a widely used, open-source programming language perfect for modern development. With its advanced type system, Typescript allows developers to write more robust, maintainable, and scalable code. But, to truly harness the power of Typescript and build high-quality projects, it’s essential to understand and follow best practices. This article will walk through the Advanced Typescript concepts and capabilities. Whether you are just starting or you are an experienced Typescript developer, this article will provide valuable insights and tips to help you write clean, efficient code.

So, grab a cup of coffee, and let’s get started on our journey to mastering Typescript!

What is Typescript?

Typescript is a strongly typed programming language that builds on Javascript. It was originally designed by Anders Hejlsberg in 2012 and is currently developed and maintained by Microsoft as an open-source project.

Typescript complies with Javascript and can be executed in any javascript runtime (e.g. a browser or server Node.js).

Typescript supports multiple programming paradigms such as functional, imperative, and object-oriented. Typescript is neither an interpreted nor a complied language.

Why Typescript?

Typescript is a strongly typed language that helps prevent common programming mistakes and avoid certain kinds of run-time errors before programming is executed.

A strongly typed language allows the developer to specify various program constraints and behaviors in the data type definitions, facilitating the ability to verify the correctness of the software and prevent defects. This is especially valuable in large-scale applications.

Some of the benefits of Typescript:

  • Static typing, optionally strongly typed

  • Type inference

  • Access to ES6 and ES7 features

  • Cross-Platform and Cross-browser Compatibility

  • Tooling support with IntelliSense

Advanced Typescript cheat sheet

Typescript is a simple language and allows developers to express types in terms of other types. However, while performing the basic tasks, having a profound understanding of how Typescript works is critical for unlocking its advanced functionality.

As we learn more about Typescript, we can utilize this knowledge to write cleaner and testable code. In this section, we are combining all the basic concepts of Typescript and its advanced features in a single cheatsheet. so here we go.

Types vs interfaces in Typescript

We have two options for defining types in Typescript: types and interfaces. One of the most frequently asked questions about whether we should use interfaces or types.

The answer to this question, like many programming questions, is that it depends. In some cases, one has a clear advantage over the other, but in many cases, they are interchangeable.

Types and type aliases

type is a keyword in Typescript that we can use to define the shape of data. The basic types in Typescript include:

  • String

  • Boolean

  • Number

  • Array

  • Tuple

  • Enum

  • Advanced types

Each has unique features and purposes, allowing developers to choose the appropriate one for their particular use case.

Type aliases in Typescript mean “a name for any type“. They provide a way of creating new names for existing types. Type aliases don’t define new types; instead, they provide an alternative name for an existing type.

Type aliases can be created using the type keyword, referring to any valid Typescript type, including primitive types:

type MyNumber = number;
type User = {
  id: number;
  name: string;
  email: string;
}

Interfaces in Typescript

In Typescript, an interface defines a contract to which an object must adhere. Below is an example:

interface Client { 
    name: string; 
    address: string;
}

Differences between types and interfaces

  • Primitive Types: Primitive types are inbuilt types in Typescripts. They include number, string, boolean, null, and undefined types. We can use define a type for a primitive type but we can’t use an interface to alias a primitive type.

      type Address = string;
      type NullOrUndefined = null | undefined;
    
  • Union types: Union types allow us to describe values that can be one of several types and create unions of various primitive, literal, or complex types. Union types can only be defined using type. There is no equivalent to a union type in an interface. But, it’s possible to create a new union type from two interfaces.

      type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk';
      interface CarBattery {
        power: number;
      }
      interface Engine {
        type: string;
      }
      type HybridCar = Engine | CarBattery;
    
  • Function types: In Typescript, a function type represents a function’s type signature. Using a type alias, we need to specify the parameters and the return type to define a function type:

      type AddFn =  (num1: number, num2:number) => number;
      interface IAdd {
         (num1: number, num2:number): number;
      }
    

    Both type and interface similarly define function types, except for a subtle syntax difference of interface using “:” vs “=>” when using type. Type is preferred because it’s short and thus easier to read.

  • Declaration merging: Declaration merging is a feature that is exclusive to interfaces. With declaration merging, we can define interfaces multiple times, and the Typescript compiler will automatically merge these definitions into a single interface definition.

    In the following example: the two Client interface definitions are merged into one by the Typescript compiler, and we have two properties when using the Client Interface.

      interface Client { 
          name: string; 
      }
    
      interface Client {
          age: number;
      }
    
      const harry: Client = {
          name: 'Harry',
          age: 41
      }
    

    Type aliases can’t be merged in the same way if you try to define the Client type more than once, as, in the above example, the error will be thrown.

    When used in the right places, definition merging can be very useful. One common use case for declaration merging is to extend the third-party type definition to fit the particular project's needs.

    If you need to merge declarations, interfaces are the way to go.

  • Extends vs. intersection: An interface can extend one or multiple interfaces. Using the extend keyword, an interface can inherit all the properties and methods of an existing interface while also adding new properties.

    For example, We can create a VipClient interface by extending the Client interface:

      interface VIPClient extends Client {
          benefits: string[]
      }
    

    To achieve a similar result for types, we need to use an intersection operator:

      type VIPClient = Client & {benefits: string[]}; // Client is a type
    

    You can extend an interface from a type alias with static known members:

      type Client = {
          name: string;
      };
    
      interface VIPClient extends Client {
          benefits: string[]
      }
    
  • Handling conflicts when extending: Another difference between types and interfaces is how conflicts are handled when you try to extend from one with the same property.

    When extending interfaces, the same property isn’t allowed, as in the example below:

      interface Person {
        getPermission: () => string;
      }
    
      interface Staff extends Person {
         getPermission: () => string[];
      }
    

    An error is thrown because a conflict is detected.

    Type aliases handle conflicts differently. In the case of type aliases extending another type with the same property key, it will automatically merge all properties instead of throwing an error.

    In the following example, the intersection operator merges the method signature of the two getPermission declarations and a typeof operator is used to narrow down the union type parameter so that we can get the return value in a type-safe way:

      type Person = {
        getPermission: (id: string) => string;
      };
    
      type Staff = Person & {
         getPermission: (id: string[]) => string[];
      };
    
      const AdminStaff: Staff = {
        getPermission: (id: string | string[]) =>{
          return (typeof id === 'string'?  'admin' : ['admin']) as string[] & string;
        }
      }
    

    It is important to note that the type intersection of two properties may produce unexpected results. In the example below, the name property for the extended type Staff becomes never, since it can’t be both number and string at the same time:

      type Person = {
          name: string
      };
    
      type Staff = person & {
          name: number
      };
      // error: Type 'string' is not assignable to type 'never'.(2322)
      const Harry: Staff = { name: 'Harry' };
    
  • Prefer extends over an intersection: Often, when using an interface, Typescript will generally do a better job displaying the shape of the interface in error messages, tooltips, and IDEs. It is also much easier to read, no matter how many types you combine or extend.

    Compare that to the type alias that uses the intersection of two or more types like type A = B & C;, and you then type to use that alias in another intersection like type X = A & D;, TypeScript can struggle to display the structure of the combined type, making it harder to understand the shape of the type from the error messages.

    Typescript caches the results of the evaluated relationship between interfaces, like whether one interface extends another or if two interfaces are compatible. This approach improves the overall performance when the same relationship is referenced in the future.

    In contrast, when working with intersections, TypeScript does not cache these relationships. Every time a type intersection is used, TypeScript has to re-evaluate the entire intersection which can lead to efficiency concerns.

    For these reasons, it is advisable to use interface extends instead of relying on type intersections.

  • Advanced type features: TypeScript provides a wide range of advanced type features that can’t be found in interfaces. Some of the unique features in TypeScript include:

    • Type inferences: Can infer the type of variables and functions based on their usage. This reduces the amount of code and improves readability

    • Conditional types: Allow us to create complex type expressions with conditional behaviors that depend on other types

    • Type guards: Used to write sophisticated control flow based on the type of variable.

    • Mapped types: Transforms an existing object type into a new type

    • Utility types: A set of out-of-the-box utilities that help to manipulate types

TypeScript’s typing system constantly evolves with every new release, making it a complex and powerful toolbox. The impressive typing system is one of the main reasons many developers prefer to use TypeScript.

When to use types vs. interfaces

In many cases, they can be used interchangeably depending on personal preference. But we should use type aliases in the following use cases:

  • To create a new name for a primitive type

  • To define a union type, tuple type, function type, or another more complex type.

  • To overload functions.

  • To use mapped type, conditional types, type guards, or other advanced type features.

Compared with interfaces, types are more expressive. Many advanced type features are unavailable in interfaces, and those features continue to grow as Typescript is involved.

Type safety in Typescript - Unknown vs any

In Typescript, two types that are often used are any vs unknown. They might look the same at first, but they are used for different things.

Exploring unknown

The unknow type is a safer choice than any. When a variable is of type unknown, Typescript makes you check the type before you can use this variable. This makes developers clearly state their assumptions, which helps to avoid errors when the code is running.

Think about a situation when you are using the data from an API, and you are not sure what the data looks like. Using an unknown type helps you deal with uncertainty in a safe way.

function processData(data: unknown): string {
  if (typeof data === 'string') {
    return data.toUpperCase();
  } else {
    // Handle other cases appropriately
    return "Invalid data";
  }
}

// No compilation error
const result = processData("Hello, TypeScript!");

In simpler terms, using unknown type ensures you double-check what kind of data you have before you do anything with it. This helps prevent mistakes that could happen when the program is running.

Exploring any

On the other hand, any is the most easy-going type in Typescript. It skips type-checking, letting variables of this type be set to anything without making the code fail to compile. While this freedom can be handy, it means you lose the advantages of static typing.

Think about a situation where you want as much flexibility as possible, and you are sure that your code is type-safe.

function processDynamicData(data: any): string {
  // No compilation error
  return data.toUpperCase(); 
}

// No compilation error, works as expected
const result1 = processDynamicData("Hello, TypeScript!"); 
console.log(result1); // Outputs: "HELLO, TYPESCRIPT!"

// No compilation error, but will cause a runtime error
const result2 = processDynamicData(12345); 
console.log(result2); // Error: data.toUpperCase is not a function

In simpler terms, using any means you don’t have to check the type of data, but it can cause problems if the data type is not what you expected.

When to use

When using Typescript, you can use unknown when you want to be very careful with your code and make sure everything is the right type. any is used when you want to be more flexible, but this can make your code less safe. You should think carefully about which one to use, depending on what your code needs and how confident you are about the types and safety of your code.

The Differences Between Object, {}, and object in Typescript

In Typescript, when we want to define an object type, there are several concise such as Object, {}, and object. what are the differences between them?

Object (uppercased)

Object (uppercased) describes properties common to all Javascript objects. It is defined in the lib.es5.d.ts file that comes with the Typescript library.

As you can see, it includes some common properties like toString(), valueOf(), and so on.

Because it emphasizes only those properties that are common to Javascript objects. So you can assign boxable objects like string, boolean, number, bigint, symbol to it, but not the other way around.

{}

{} describes an object that has no members on its own, which means Typescript will complain if you try to access its property members.

From the code example above, we can see that {} and Object (uppercased) have the same features. That is, it can only access those properties that are common (even if the Javascript code logic is correct), and all boxable objects can be assigned to it, etc.

This is because the {} type can access those common properties through the prototype chain, it also has no own properties. So it behaves the same as the Object (uppercased) type. But they represent different concepts.

object (lowercased)

object (lowercased) means any no-primitive type, which is expressed in code like this:

type PrimitiveType =
  | undefined
  | null
  | string
  | number
  | boolean
  | bigint
  | symbol;

type NonPrimitiveType = object;

This means that all primitive types are not assignable to it, and vice versa:

Using never type

In Typescript, never is a special type that represents value that will never occur. It’s used to indicate that a function will not return normally, but will instead throw an error. This is a great way to identify to other developers and the compiler that a function can be used in certain ways, this can help to catch potential bugs.

For example, consider the following function that throws an error if the input is 0:

function divide(numerator: number, denominator: number): number {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

Here, the function divide is declared to return a number, but if the denominator is zero, it will throw an error. To indicate that this function will not return normally in this case, you can use never as a return type:

function divide(numerator: number, denominator: number): number | never {
 if (denominator === 0) {
   throw new Error("Cannot divide by zero");
 }
   return numerator / denominator;
}

Using Enums

Enums, short for enumerations, are a way to define a set of named constants in Typescript. They can be used to create a more readable and maintainable code, by giving a meaningful name to a set of related values.

For example, you can use an enum to define a set of possible status values for an order:

enum OrderStatus {
 Pending,
 Processing,
 Shipped,
 Delivered,
 Cancelled
}

let orderStatus: OrderStatus = OrderStatus.Pending;

Enums can also have a custom set of numeric values or strings:

enum OrderStatus {
 Pending = 1,
 Processing = 2,
 Shipped = 3,
 Delivered = 4,
 Cancelled = 5
}

let orderStatus: OrderStatus = OrderStatus.Pending;

Always name an enum with the first capital letter and the name has to be in singular form, as a part of the naming conversation.

Read this article to explore the cases of Typescript Enums that are used in the real world.

Using Namespaces

Namespaces are a way to organize your code and prevent naming collisions. They allow you to create a container for your code, where you can define variables, classes, functions, and interfaces.

For example, you can use a namespace to group all the code related to a specific feature:

namespace OrderModule {
 export class Order { /* … */ }
 export function cancelOrder(order: Order) { /* … */ }
 export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

You can also use namespaces to prevent naming collisions by providing a unique name for your code:

namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}

let myClass = new MyCompany.MyModule.MyClass();

It’s important to note that namespaces are similar to modules, but they are used to organize the code and prevent naming collisions, while modules are used to load and execute the code.

Using Typescript infer Like a Pro

Do you know how to get the type of the elements in the T0 array type and the return value type in the T1 function type? Give yourself a few seconds to think about it.

type T0 = string[];
type T1 = () => string;

We can use the type pattern matching technology provided by Typescript - conditional types and infer to complete the previous requirements.

Conditional types allow us to detect the relationship between 2 types and determine whether they are compatible. Infer is used to declare a type variable to store the type captured during pattern matching.

Let’s see how to capture the type of the elements in the T0 array type:

type UnpackedArray<T> = T extends (infer U)[] ? U : T
type U0 = UnpackedArray<T0> // string

In the above code, T extends (infer U)[] ? U : T is the syntax for conditional types, infer U in the extended clauses introduces a new type variable U to store the inferred type.

For a better understanding, Let’s demonstrate the execution flow of the UnpackedArray utility type.

It should be noted that infer can only be used in the extends clause of the conditional type, and the type variable declared by infer is only available in the true branch of the conditional type.

type Wrong1<T extends (infer U)[]> = T[0] // Error
type Wrong2<T> = (infer U)[] extends T ? U : T // Error
type Wrong3<T> = T extends (infer U)[] ? T : U // Error

For more use cases and more explanations, refer to this article.

Using Typescript Conditional Types Like a Pro

Have you used the Exclude, Extract, NonNullable, Parameters, and ReturnType utility types? Do you know how they work internally? In fact, the above Typescript built-in utility types are all developed based on Conditional Types.

Here we first briefly understand the specific implementation of these Typescript built-in utility types.

These utility types are used for the following purposes:

  • Exclude: Constructs a type by excluding it from UnionType all union members that are assigned to ExcludedMembers.

  • Extract: Constructs a type by extracting from Type all union members that are assigned to Union.

  • NonNullable: Constructs a type by excluding null and undefined from Type.

  • Parameters: Constructs a tuple type from the types used in the parameters of a function type Type.

  • ReturnType: Constructs a type consisting of the return type of the function Type.

Here we look at a few usage examples:

if you want to master them thoroughly and create you own utility types.

The built-in utility types described earlier use the Conditional Types introduced in Typescript 2.8 internally. The syntax for this type is as follows:

T extends U ? X : Y

So what are the uses of conditional types? Let’s take an example here:

type IsString<T> = T extends string ? true : false;
​
type I0 = IsString<number>;  // false
type I1 = IsString<"abc">;  // true
type I2 = IsString<any>;  // boolean
type I3 = IsString<never>;  // never

Refer this article to explore more.

What are K, T, and V in Typescript Generics?

Does it sound strange when you first see the T in Typescript generics?

The T in the figure is called a generic type parameters, and it is a type placeholder we wish to pass the identity function.

Just like passing parameters, we take the actual type specified by the user and chain it to the parameter type and return the value type.

So what does T means? The generic type parameter T in the figure represents Type, in fact, T can be replaced by any valid name. In addition to T, common generic variables are K, V, E, etc.

  • K(Key): represents a type of key in an object.

  • V(Value): represents a type of value in an object.

  • E(Element): represents the element type.

Of course, you don’t have to define only one type parameter, you can introduce any number of type parameters. Here we introduce a new parameter type U that extends the identity function we defined.

When calling the identity function, we can explicitly, specify the actual type of the generic parameter. Of course, you can also not specify the type of the generic parameter, and let Typescript automatically complete the type reference for us.

Using Typescript Mapped Types Like a Pro

Have you used the Partial, Required, Readonly, and Pick utility types?

Do you know how they work internally? if you want to master them thoroughly and create your own utility types, don’t miss the content below :)

User Registration is a widespread scenario in daily work. Here we can use Typescript to define User in which all keys are required.

type User = {
  name: string; 
  password: string; 
  address: string; 
  phone: string;
};

Usually, for registered users, we allow users to modify only some user information. At this point, we can define a new UserPartial type representing the object type to update, in which all keys are optional.

type UserPartial = {
  name?: string; 
  password?: string; 
  address?: string; 
  phone?: string; 
};

For the scenario of viewing user information, we hope that all keys of the object type corresponding to the user object are read-only. For this requirement, we can define the Readonly User type.

type ReadonlyUser = {
  readonly name: string;
  readonly password: string;
  readonly address: string;
  readonly phone: string;
};

Reviewing the 3 user-related types already defined, we will see that they contain a lot of duplication code.

So how can we reduce those duplication codes in the above types? The answer is we can use the mapped type, which is a generic type that can be used to map the original object type to a new object type.

The syntax for mapped types is as follows:

Where P in K is similar to the Javascript for … in the statement, which is used to iterate through all types in type K, and the T type variable, which is used to represent any type in Typescript.

You can also use the additional modifiers read-only and question mark (?) in the mapping process. The corresponding modifiers are added and removed by adding the plus(+) and minus(-) prefixes. The default is to use the plus sign if no prefix is added.

We can now summarize the syntax of the common mapped type.

{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

After introducing the syntax of common mapped types, let’s look at some examples:

Let’s look at how to redefine the PartialUser type using mapped types:

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;

In the above code, we define the MyPartial type mapped type and then use it to map the User type to the UserPartial type. The keyof operator is used to get all the keys of a type, and its return type is a union type. The type variable P changes to a different type with each traversal, T[P], which is similar to the syntax for attribute access, and is used to get the type of the value corresponding to an attribute of the object type.

Let’s demonstrate the complete execution flow of MyPartial mapped types, if you are not sure, you can watch it several times to deepen your understanding of the Typescript mapped type.

Refer to this article to explore more.

Using Typescript Template Literal Types Like a Pro

When developing web pages, we usually use Tooltip or Popover to display prompt messages or explanatory information. In order to meet some usage scenarios, Tooltop or Popover will allow the user to set their placement position. For example, top, bottom, left, right, etc.

Since string literal types can basically spell-check our string values, we define a Side type using Typescript’s type aliases.

type Side = 'top' | 'right' | 'bottom' | 'left';
let side: Side = "rigth"; // Error
// Type '"rigth"' is not assignable to type 'Side'. 
// Did you mean '"right"'?ts(2820)

For the above 4 positions, it can already meet most scenarios. But if you want to set the placement position of the Tooltip more precisely, for example, let the Tooltip display in the upper area of the specified elements:

Then the existing Side can’t meet the requirements, let’s define the new Placement type:

type Placement = Side
  | "left-start" | "left-end" 
  | "right-start" | "right-end" 
  | "top-start" | "top-end" 
  | "bottom-start" | "bottom-end"

In the Placement type, in addition to the original 4 positions, we add 8 new positions such as “left-start“, “left-end“, and “right-start“, which correspond to the 8 string literal types. Looking at these string literal types, we find some duplicate code “-start“, and “-end“. In addition, when defining these literal types, spelling errors may occur if you are not careful.

So how can the above problems be solved better? This is where we can use the new template literal types introduced in Typescript 4.1, which is used in the following way:

type Alignment = 'start' | 'end';
type Side = 'top' | 'right' | 'bottom' | 'left';
type AlignedPlacement = `${Side}-${Alignment}`;
type Placement = Side | AlignedPlacement;

After reading the above code, do you think it is much simpler? Similar to template strings in Typescript, template literal types are enclosed in backticks and can contain placeholders of the form ${T}. The actual type of the type variable T can be string, number, boolean, or bigint.

Template literal types provide us with the ability to concatenate string literals and convert literals of non-string primitive types to their corresponding string literal types. Here are some examples:

Refer to this article to explore more.

The Purpose of declare Keyword in Typescript

When you open the *.d.ts file in Typescript projects, you may see delcare. Do you know what delcare does?

In Typescript projects, you may import third-party Js-SDK in the form of script tags, such as importing Js-SDK of the Google Maps platform.

<script
   src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly" defer></script>

After initialization, you will call the API exposed by the Js-SDK in a Typescript file.

Although you are using the API provided by Js-SDK according to the Google Maps development documentation, the Typescript compiler still prompts the corresponding error message for the above code. This is because the Typescript compiler does not recognize the global variable google.

So how do we solve this problem? The answer is to use the declare keyword to declare the global google variable so that the Typescript compiler can recognize the global variable.

declare var google: any;

Seeing this, do you get confused? Why can you use global variables like JSON, Math, or Object in Typescript projects normally? This is because Typescript makes the declaration for us internally, and the global variables mentioned earlier are declared in the lib.es5.d.ts declaration file.

// typescript/lib/lib.es5.d.ts
declare var JSON: JSON;
declare var Math: Math;
declare var Object: ObjectConstructor;

In fact, in addition to declaring global variables, the declare keyword can also be used to declare global functions, global classes, and global enum types. Functions such as eval, isNaN, encodeURI, and parseInt that you may have used at work also declare in the lib.es5.d.ts declaration file.

declare function eval(x: string): any;
declare function isNaN(number: number): boolean;
declare function encodeURI(uri: string): string;
declare function parseInt(string: string, radix?: number): number;

It should be noted that when declaring a global function, we do not include the specific implementation of the function. With the declaration file, the Typescript can recognize the global Javascript functions.

Explore more here.

How To Define Object Type With Unknown Structures in Typescript

Did you encounter similar errors when you were learning Typescript?

To fix this error, use a very violent way is to use any type:

let user: any = {}
user.id = "TS001";
user.name = "Bytefer";

Beside using any type, how many solutions do you know? In this article, I will introduce 3 other solutions. Before you continue reading, I suggest you take a moment to think about it.

One of solutions is to use type or interface to define a User type:

interface User {
  id: string;
  name: string;
}
let user = {} as User;
user.id = "TS001";
user.name = "Bytefer";

Although using the User type, the previous problem can be solved. But you set a new age property for the user object, the following error message will be displayed:

Property 'age' does not exist on type 'User'.ts(2339)

So how should we solve the problem of dynamic property assignment? At this point, we can use TypeScript’s index signatures. When we only know the type of the object keys and values, we can use index signatures to define the type of that object. The syntax of the index signatures is as follows:

The type of the key can only be string, number, symbol, or template literal type, while the type of the value can be any type.

The template literal types is a new type introduced in TypeScript 4.1, and in combination with index signatures, we can define more powerful types.

In addition to using index signatures, we can also use TypeScript’s built-in utility type Record type to define the User type. The role of the Record utility type is as follows:

So what’s the difference between index signatures and a Record utility type? In some cases, they all define the expected type.

const user1: Record<string, string> = { name: "Bytefer" }; // Ok
const user2: { [key: string]: string } = { name: "Bytefer" }; // Ok

For index signatures, the key type can only be string, number, symbol, or template literal type. For the Record utility type, the key type can be a literal type or a union of literal types:

5 Very Useful Tricks for Typescript Typeof Operator

In Javascript, you can get the type of a variable through typeof operator, do you know what the typeof operator is used in Typescript? I will introduce 5 common application scenarios of the typeof operator, which you may use in future projects.

Get the type of object

The man object is a regular JavaScript object. In Typescript, you can use an interface or type to define the type of object. With this object type, you can use TypeScript’s built-in utility types, such as Partial, Required, Pick, or Readonly, to handle object types to meet different needs.

For simple objects, this may not be a big deal. But for large, complex objects with deeper nesting levels, manually defining their types can be mind-numbing. To solve this problem, you can use typeof operator.

type Person = typeof man;
type Address = Person["address"];

Compared to manually defining the type before. It becomes much easier to use the typeof operator. Person["address"] is an indexed access type used to look up a specific property (address) on another type (Person type).

Get a Type That Represents All Enum Keys as Strings.

In Typescript, enum types are special types that get combined into regular Javascript objects.

Therefore, you can also use the typeof operator on enum types. But this is often not of much practical use, and when dealing with enum types, it is usually combined with the keyof operator.

Get The Type of The Function Object

There is another more common scenario in which the typeof operator is used in your work. After obtaining the corresponding function type, you can continue to use Typescript’s built-in ReturnType and Parameters utility types to obtain the function’s return value type and parameter type, respectively.

Get the Type of the Class Object

Since the typeof operator can handle the function object, can it handle Class objects? The answer is yes.

In the above code, createPoint is a factory function that creates an instance of the Point class. Through the typeof operator, you can obtain the corresponding construct signature of the Point class, so as to realize the corresponding type verification. When defining the parameter type of Constructor, if the typeof operator is not used, the following error message will appear:

Get a More Precise Type

When using the typeof operator, if you want to get more a precise type, then you combine it with the cost assertion introduced in Typescript version 3.4. This is used in the following way.

As you see in the above figure, after using the cost assertion, and then using the typeof operator, you can obtain a more precise type.

Typescript Visualized: 15 Most Used Utility Types

In the process of using Typescript, we are programming type-oriented. In order to meet several work scenarios, we need to transform the known type. For the convenience of Typescript users, the Typescript team has provided us with many built-in utility types. With these utility types, we can easily convert types, extract types, exclude types, or get the parameter type or return type value of the function.

I picked 15 very useful utility built-in types and introduced their usage and internal working principles in the form of images or animations. After that, I believe you can really master the usage of these utility built-in types.

Partial<Type>

Constructs a type with all properties of Type set to optional.

/**
 * Make all properties in T optional. 
 * typescript/lib/lib.es5.d.ts
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

Required<Type>

Constructs a type consisting of all properties of Type set to required. The opposite of Partial.

/**
 * Make all properties in T required.
 * typescript/lib/lib.es5.d.ts
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

For more utility types, explore here.

Clean React With Typescript

Today, I use Typescript on a daily basis and actually like it quite a bit, even though I regularly break the compiler. One thing that took me quite some time though, was figuring out the best way to add Typescript to all the React patterns and features. I have seen it done in so many ways ending up confused by all the different types in the library itself.

To help you avoid that struggle or/and to improve your existing codebase, this article explores different use cases and - in my opinion - most elegant solutions.

Components

First of all, let’s talk about components.

They are the heart of every React application and probably the thing you write the most often. Even since the introduction of hooks, most components are plain Javascript functions that take a set of props and return some markup - usually in the form of JSX.

Given that, typing them is definitely straightforward. The only real constraint is that it always takes a single argument - an object of properties to be precise.

Basic Props

Let’s start with some primitive props first. In this example, we have a component that takes a title and a description. Both are string and description is optional.

type Props = {
  title: string
  description?: string
}

function ProductTile({ title, description }: Props) {
  return (
    <div>
      <div>{title}</div>
      {description && <div>{description</div>}
    </div>
  )
}

Children

So far, so good. Now a common use case is to pass children to a component in order to render nested components. You might be inclined to add it to your type, but React actually provides a special type that could help you reduce redundancy.

It’s called PropsWithChildren and it’s a generic type that takes your existing props type and adds the children for you.

import { ReactNode, PropsWithChildren } from 'react'

type Props = {
  title: string
}

function ProductTile({ title, children }: PropsWithChildren<Props>) {
  return (
    <div>
      <div>{title}</div>
      {children}
    </div>
  )
}

Tip: This argument is optional. If your component only takes children, you can pass PropsWithChildren on its own.

Piping Props

Another common thing to do is piping props in inner components. For example, you might have a component that itself renders our ProductTile component, but also accepts additional props for customization.

import ProductTile from './ProductTile'

type Props = {
  color: string
  title: string
}

function ProminentProductTile({ color, title }: Props) {
  return (
    <div style={{ background: color }}>
      <ProductTile title={title} />
    </div>
  )
}

While this is totally fine for primitive types such as strings or numbers, it can become cumbersome to repeat those types if you are dealing with complex records or functions - especially you have to pass value through multiple components.

Remember the DRY rule? Instead of repeating the same types over and over, we can leverage another generic provided by React which is ComponentProps. It takes the type of a component that we can get by typeof and returns its props type. we can then use indexed access to get the specific value.

import { ComponentProps } from 'react'

import ProductTile from './ProductTile'

type ProductTileProps = ComponentProps<typeof ProductTile>
type Props = {
  color: string
  title: ProductTileProps['title']
}

You can argue that we can achieve the same by simply exporting the type from ProductTile. And you are right, that works too, but it’s also more prone to errors if you change your types and/or extend them at some point.

Spreading Props

Similar to piping props, sometimes we want to spread all extra props to some underlying component. Imagine the ProminentProductTile should pipe both title and description.

Instead of specifying each property one by one, we can simply extend the type we can leveraging ComponentProps once again.

import { ComponentProps } from 'react'

import ProductTile from './ProductTile'

type Props = {
  color: string
} & ComponentProps<typeof ProductTile>

function ProminentProductTile({ color, ...props }: Props) {
  return (
    <div style={{ background: color }}>
      <ProductTile {...props} />
    </div>
  )
}

Tip: if you only want to spread a subset of those types, you can use a Typescript built-in Pick to do so. For example:

type ProductTileProps = ComponentProps<typeof ProductTile>

type Props = {
  color: string
} & Pick<ProductTileProps, 'title' | 'description'>

HTML elements

The same pattern also applies to HTML primitives. if you want to pass down all remaining props to e.g button, we can simply use ComponentProps<“button“>.

Note: There's also React.JSX.IntrinsicElements["button"] which refers to the identical type, but I would recommend you to use only one for consistency and readability and I generally prefer the first as it’s easier to use.

Extra: For certain edge cases where we only want to pass down valid HTML attributes - excluding React specific props such as ref and key - we can also use specific attribute types e.g. React.ButtonHTMLAttributes<HTMLButtonElement>. However, I have not yet encountered a use case for those as it’s once again more to type, I still prefer the shorter ComponentProps<“button“>.

Passing JSX

Now that we have covered simple props, let’s look at some advanced use cases. Sometimes, passing primitive props is not enough and we want to pass down the raw JSX to render nested content. While dependency injection generally makes your component less predictable, it’s a great way to customize generic components. This is especially useful, when we are already passing children, but need to inject additional makeup at a specific position.

Luckily, React provides us a another useful type called ReactNode. Imagine a layout component that wraps your whole application and also receives the sidebar to render a dedicated navigation in a predefined slot.

import { PropsWithChildren, ReactNode } from 'react'

type Props = {
  title: string
  sidebar: ReactNode
}

function Layout({ title, children, sidebar }: PropsWithChildren<Props>) {
  return (
    <>
      <div className="sidebar">{sidebar}</div>
      <main className="content">
        <h1>{title}</h1>
        {children}
      </main>
    </>
  )
}
// Now we can pass whatever we want
const sidebar = (
  <div>
    <a data-selected href="/shoes">
      Shoes
    </a>
    <a href="/watches">Watches</a>
    <a href="/shirts">Shirts</a>
  </div>
)

const App = (
  <Layout title="Running shoes" sidebar={sidebar}>
    {/* Page content */}
  </Layout>
)

Bonus: PropsWithChildren is actually using ReactNode under the hood. A custom implementation would look something like this:

type PropsWithChildren<Props = {}> = { children: ReactNode } & Props

Passing Components

Passing JSX is great when you want maximum flexibility. But what if you want to restrict rendering to certain components or pass some props to that subtree as well without moving all the logic into a single component bloating it even more?

What became popular as the render-prop pattern(new tab) is exactly that: dependency injection with constraints.
Once again, React has a neat type to help us with that called ComponentType.

import { ComponentType } from 'react'

type ProductTileProps = {
  title: string
  description?: string
}

type Props = {
  render: ComponentType<ProductTileProps>
}

function ProductTile({ render }: Props) {
  // some logic to compute props

  return render(props)
}

Extra: This is also quite nice for third-party components via d.ts files:

declare module 'some-lib' {
  type Props = {
    title: string
    description?: string
  }

  export const ProductTile: ComponentType<Props>
}

Specific Components

If we only allow a specific component, the code is even simpler as we can use the built-in typeof operator(new tab) to do so.

import { ComponentProps } from 'react'

import Icon from './Icon'

type Props = {
  icon?: typeof Icon
} & ComponentProps<'button'>

function Button({ icon: Icon, children, ...props }: Props) {
  return (
    <button {...props}>
      {icon && <Icon size={24} />}
      {children}
    </button>
  )
}

Inferring Props

This is a common pattern used for generic (layout) components. It's often used in component libraries and can be a great foundation for creating flexible layouts in React.

Usually, you pass an as or component prop and the component becomes that, including its props and types. For example:

const App = (
  <>
    {/* this throws as div doesn't have a value prop */}
    <Box as="div" value="foo" />
    {/* this works however */}
    <Box as="input" value="foo" />
  </>
)

The best part is that it also works with custom components, giving us endless flexibility.
Alright, but how do we achieve this? Instead of explaining what Matt Pocock(new tab) from Total TypeScript(new tab) already did with a great article, I'm just going to link it here: Passing Any Component as a Prop and Inferring Its Props(new tab).

\=> For more cases of React in Typescript, explore them here.

Using Typescript Type Predicates for Precise Type Checking

Type predicates are a powerful tool used to check and ensure at runtime that a variable belongs to a specific type. By using type predicates, we can achieve more precise type checking when writing type-safe code, thereby avoiding type errors and enhancing the robustness and maintainability of the code.

Suppose we have a union type representing to animals, including cats(Cat) and dogs (Dog).

interface Cat {
  kind: "cat";
  meow: () => void;
}

interface Dog {
  kind: "dog";
  bark: () => void;
}

type Animal = Cat | Dog;

Now, we want to write a function to check whether an Animal is of type Cat. At this point, we can use the type predicate:

function isCat(animal: Animal): animal is Cat {
  return animal.kind === "cat";
}

function makeSound(animal: Animal) {
  if (isCat(animal)) {
    animal.meow(); // Here, TypeScript knows that animal is of type Cat.
  } else {
    animal.bark(); // Here, TypeScript knows that animal is of type Dog.
  }
}

In the example above, we define a function name isCat and use the type predicate animal is Cat as the return type. The function checks if the kind property of the passed animal object is equal to “cat”. If it is, the function returns to true and informs the Typescript compiler that within this conditional branch, the animal variable is of type Cat.

By doing this, the makeSound function can accurately identify the specific type of Animal and call the methods specific to Cat or Dog in the corresponding conditional branches. This not only enhances the type safety of the code but also makes the code cleaner and easier to maintain.

Typescript Index Signature Explained

An index signature is defined using square brackets [] and the type of keys, followed by the colon and the type for the corresponding values. It enables Typescript to understand and enforce the expected structure of the object.

interface MyStats {
  [key: string]: number;
}
const scores: MyStats = {
  total: 50,
  average:80
}
// index siganture enforce the type constraint
// here, the value must be a number
const scores2: MyStats = {
  total: "50", //Type 'string' is not assignable to type 'number'.(2322)
  average:80
}

Note that index signatures can use different key types, such as string, number, symbol, or literal type and the associated value type can be any valid Typescript type.

Mixing an index signature with explicit members

In Typescript, we can mix an index signature with explicit member declarations. It’s helpful for cases requiring a combination of known and dynamic properties.

interface CarConfiguration {
  [feature: string]: number;
  price: number;
}

When we mix index signatures with explicit members, all explicit members need to conform to the index signature types.

// invalid case
interface CarConfiguration {
  [feature: string]: number;
  price: number;
  model: string; // Error: Property 'model' of type 'string' is not assignable to 'string' index type 'number'
}

// valid
interface CarConfiguration {
  [feature: string]: number | string;
  price: number;
  model: string;
}

Readonly index signature

Index signature supports readonly modifier. By applying readonly modifier, the properties in the object will be immutable.

interface Car {
  readonly [key: string]: boolean;
}

const toyota: Car = {hybrid: true, luxury: false};
toyota.hybrid = false; //Error: Index signature in type 'Car' only permits reading.(2542)

In the above example, an error occurs when trying to modify hybrid property because the interface only allow for reading, not writing.

How to use index signature

Let’s consider a real-world example of how index signatures can be used. Imagine you are developing an application with various features. Each feature includes its own set of setting, which can also enable or disable.

interface FeatureConfig {
  [feature: string]: {
    enabled: boolean;
    settings: Record<string, boolean>;
  }
}

In this example, we define an interface named FeatureConfig. It uses an index signature to allow dynamic property names of type string associated with anenabled boolean property and a settings object. It is handy for representing configurations with dynamic feature names and associated settings. For example, we can apply the interface to the following object.

const features: FeatureConfig = {
  profile: {
    enabled: true,
    settings: {
      showPhoto: true,
      allowEdit: false,
    },
  },
  notification: {
    enabled: false,
    settings: {
      richText: true,
      batchMode: true
    },
  }
};

In the features object, the feature names can vary, but the structure for each feature remains consistent. Each feature is expected to have an enabled boolean and a settings object.

To improve the type safety, can we apply a union-type constraint to the feature name in the above interface?

If the set of features in our application is known, we can define the union of string literals namedFeatureType.

The key of the index signature does not support the union type, but we can work around it using a mapped type.

type FeatureType = 'profile' | 'notification' | 'reporting';

type FeatureConfig2 = {
  [feature in FeatureType]: {
    enabled: boolean;
    settings: Record<string, boolean>;
  }
}

[feature in FeatureType]is a mapped type that iterates over each string literal in the union type FeatureType (which includes 'profile', 'notification', and 'reporting'), and it uses each value as the resulting type's property name.

Here’s an example of how we might use it:

const allFeatures: FeatureConfig2 = {
  profile: {
    enabled: true,
    settings: {
      showPhoto: true,
      allowEdit: false,
    },
  },
  notification: {
    enabled: false,
    settings: {
      richText: true,
      batchMode: true
    },
  },
    reporting: {
    enabled: true,
    settings: {
      template: false,
      advanceExport: true
    },
  },
};

Note that we need to include all features defined in FeatureType to the object to match the type expectations.

If we want to allow a subset of the features as the key, we need to modify the index signature type with an “?” as an optional flag. Then, we could use the FeatureConfig2 type for an object that only contains a subset of features.

type FeatureType = 'profile' | 'notification' | 'reporting';

type FeatureConfig2 = {
  [feature in FeatureType]?: {
    enabled: boolean;
    settings: Record<string, boolean>;
  }
}

const subsetFeatures: FeatureConfig2 = {
  profile: {
    enabled: true,
    settings: {
      showPhoto: true,
      allowEdit: false,
    },
  }
};

Index signature and Record type

A common question about index signature is “Why not use record type instead?“.

Record type defines an object type where we know the specific set of properties and their corresponding types. For example below, the UserRole type restricts the keys to “admin”, “editor” or “viewer”.

type UserRole = "admin" | "editor" | "viewer";

type UserPermissions = Record<UserRole, boolean>;

const permissions: UserPermissions = {
  admin: true,
  editor: false,
  viewer: true,
};

For index signature, we don’t know the specific properties; we only know the general type of property types. Thus, the use case of the index signature is to model dynamic objects, and the record type can be used for known object types.

Using Typescript Recursive Like a Pro

A linked list is a common data structure, that contains a series of nodes, each node contains two parts: data and a pointer to the next node. The first node in the linked list is called the head node and the last node is called the tail node.

So how do we define a linked table type in Typescript? if you want to use an type or interface to define a linked table type, we need to use the recursive type in Typescript. A recursive type is a type that can refer to itself. This type is useful when dealing with recursive structures because it allows us to define a type that can refer to itself to represent each node of a recursive structure.

Let’s take a look at how to use recursive types to define the type of singly linked list:

interface ListNode<T> {
  data: T;
  next: ListNode<T> | null;
}

interface LinkedList<T> {
  head: ListNode<T> | null;
  tail: ListNode<T> | null;
  length: number;
  get(index: number): ListNode<T> | null;
  insert(index: number, value: T): boolean;
  remove(index: number): ListNode<T> | null;
}

In the above code, ListNode<T> represents the type of the chain node, and contains two properties, data and next, where the data property is used to store the value of the node, and the next property is used to store the pointer to the next node. Note that when the next property is not empty, it is of type ListNode<T>, which uses Typescript’s recursive types. And LinkedList<T> represents the type of a singly linked list, which contains the head node and the tail node, as well as the length property and some methods to manipulate the list.

For more details, refer to this article.

Typescript Type Guards

A type guard is an expression that performs a runtime check that guarantees the type in some scope. A typical application scenario for type guarding is to narrow the type scope of a union type. This is to ensure type safety, that is, to safely access specific properties or methods in a particular type of object during runtime.

typeof type guards

First, let’s introduce the more common typeof type of guards. The typeof operator can obtain the type of an object at runtime, and the operator returns the following possible values.

  • "string"

  • "number"

  • "bigint"

  • "boolean"

  • "symbol"

  • "undefined"

  • "object"

  • "function"

So using the typeof operator, we can get the actual of a variable at runtime. Let’s take an example:

function printId(id: string | number) {
  if (typeof id === "string") {
    console.log(`ID: ${id.toUpperCase()}`);
  } else if (typeof id === "number") {
    console.log(`ID: ${id}`);
  }
}

So why use the typeof operator to narrow down the type of the id parameter? The main reason is to ensure the type safety at runtime. For example, when the type of id parameter is a numeric type, but we call the id.toUpperCase() method, a runtime exception will be thrown.

In editors that support Typescript IntelliSense, you can access certain properties of the id parameter, you can access common properties of the string and number types. Specifically as shown in the picture below:

instanceof type guards

Although the typeof operator can distinguish different types if we want to determine whether an object is an instance of a certain class, so as to safety access the unique properties or methods on the instance, then the typeof operator can do nothing up. For this requirement, we can use instanceof operator. Again, let’s take a concrete example:

class Shape {
  constructor(public id: string) {}
}

class Circle extends Shape {
  constructor(
    public id: string, 
    public radius: number) {
   super(id);
  }
}

class Square extends Shape {
  constructor(
    public id: string, 
    public sideLength: number) {
      super(id);
  }
}

In the above code, we define a Shape class and create two subclasses based on it. Next, we define a printShapeInfo function to print information about different shapes:

function printShapeInfo(shape: Shape) {
  if (shape instanceof Circle) {
    console.log(`Circle's radius is: ${shape.radius}`);
  } else if (shape instanceof Square) {
    console.log(`Square's sideLength is: ${shape.sideLength}`);
  }
}

in type guards

For the previous example of using the instanceof operator to implement type guards, we can also use the form of interfaces to describe the Shape, Circle and Square types.

interface Shape {
  id: string;
}

interface Circle extends Shape {
  radius: number;
}

interface Square extends Shape {
  sideLength: number;
}

Because the type defined by the TypeScript interface does not generate the corresponding type after compilation, we cannot use the instanceof operator for type detection at runtime. To realize the functions of the printShapeInfo function, we can use the in operator, the specific implementation is as follows:

function printShapeInfo(shape: Shape) {
  if ("radius" in shape) {
    console.log(`Circle's radius is: ${shape.radius}`);
  } else if ("sideLength" in shape) {
    console.log(`Square's sideLength is: ${shape.sideLength}`);
  }
}

user-defined type guards

To demonstrate user-defined type guards, let’s redefine the 3 types:

interface Shape {
  id: string;
}

interface Circle extends Shape {
  radius: number;
}

interface Square extends Shape {
  sideLength: number;
}

After defining the types related to Shape, let’s define the user-defined type guards function:

function isCircle(shape: Shape): shape is Circle {
  return "radius" in shape;
}

function isSquare(shape: Shape): shape is Square {
  return "sideLength" in shape;
}

Compared with ordinary functions, custom type guard functions return type predicates. shape is Circle in the above code is the so-called type predicate. A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature. You can understand the role of the isCircle user-defined type guard function in this way. If the return value of the function is true, the type of the shape parameter is the Circle type.

Now that we have the isCircle and isSquare functions, we can use them in the printShapeInfo function like this:

function printShapeInfo(shape: Shape) {
  if (isCircle(shape)) {
    console.log(`Circle's radius is: ${shape.radius}`);
  } else if (isSquare(shape)) {
    console.log(`Square's sideLength is: ${shape.sideLength}`);
  }
}

equality narrowing type guards

In addition to the 4 type guarding methods described earlier, TypeScript also supports the use of if/switch statements and equality checks, such as, ===, !===, == and != operators to narrow the types of variables.

function printValues(a: string | number, b: string | string[]) {
  if (a === b) {
    console.log(a.toUpperCase()); // (parameter) a: string
    console.log(b.toUpperCase()); // (parameter) b: string
  } else {
    console.log(a); // (parameter) a: string | number
    console.log(b); // (parameter) b: string | string[]
  }
}

In the above code, printValues function supports a and b 2 parameters, and their types are union types. When the a===bexpression evaluates to true, the types of the parameters a and b will be narrowed to string type. Of course, using the !==operator can also be used to achieve type narrowing.

function printValues2(a: string | number, b: string | string[]) {
  if (a !== b) {
    console.log(a); // (parameter) a: string | number
    console.log(b); // (parameter) b: string | string[]
  } else {
    console.log(a.toLowerCase()); // (parameter) a: string
    console.log(b.toLowerCase()); // (parameter) b: string
  }
}

Conclusion

I hope that you have found this article helpful and that it has inspired you to become a better Typescript developer.

Happy coding ❤️

References

https://blog.logrocket.com/types-vs-interfaces-typescript

https://dev.to/sachinchaurasiya/type-safety-in-typescript-unknown-vs-any-55c0?context=digest

https://dev.to/zacharylee/the-differences-between-object-and-object-in-typescript-f6f?context=digest

https://itnext.io/mastering-typescript-21-best-practices-for-improved-code-quality-2f7615e1fdc3

https://programming.earthonline.us/what-are-k-t-and-v-in-typescript-generics-9fabe1d0f0f3

https://programming.earthonline.us/with-these-articles-you-will-not-be-confused-when-learning-typescript-d96a5c99e229#16ee

https://javascript.plainenglish.io/5-very-useful-tricks-for-thetypescript-typeof-operator-404c0d30cd5

https://javascript.plainenglish.io/how-to-make-your-typescript-code-more-elegant-73645401b9b1

https://levelup.gitconnected.com/typescript-index-signature-explained-b040a78a0467

https://levelup.gitconnected.com/typescript-type-guards-in-6-minutes-9a9bab7fbe78