Type Inference
Modular Types

Modular Types

Type modularization refers to the practice of organizing and managing types in a way that promotes reusability and maintainability, often through the use of import and export statements. To facilitate this process, the @ibnlanre/builder package provides ways to access the type of values stored within the builder, or the type of a nested property, without the need for manual type declarations.

Accessing types

Although it is possible to inspect the type of the register by hovering over the builder object in an IDE, having a way to access the type programmatically is essential.

The @ibnlanre/builder package provides three primary ways to retrieve the type of the values stored within the register. Each method has its unique characteristics and use cases. This guide explains the various methods in detail, and which is best suited for a given scenario.

An example of a builder object created from a register is shown below:

import { createBuilder } from '@ibnlanre/builder';
 
const register = {
  user: {
    name: 'John Doe',
    age: 42,
  },
};
 
const builder = createBuilder(register);

Checking the register

The type of the values stored in the register can be accessed directly using the typeof (opens in a new tab) operator. This is useful when the register exists within the location of use. In scenarios where the type of the register is required outside the file it is defined in, exporting the register and importing it in the desired location is not advised. A better approach is to access the type of the register directly from the builder object.

// Access the type of the register directly.
type UserAge = typeof register.user.age;
//   ^? number

Using the use method

The $use property is a read-only attribute on the root node of the builder object, that returns the inferred type of the register. Unlike the $use and $get methods on other nodes, it is not a function and cannot be invoked. The $use property not only represents the register but also yields the anticipated result when accessed, ensuring the integrity of the builder's inferred type.

The following code snippet demonstrates how to retrieve the type of the register using the $use property:

// Access the type of the register using the `$use` property.
type Register = typeof builder.$use;
//   ^? { name: string; age: number; }
 
// Access the type of a nested property using the `typeof` operator.
type UserName = typeof builder.$use.user.name;
//   ^? string

Had the $use property on the root node been a function, accessing the type of the register would require the use of the typeof (opens in a new tab) operator coupled with the ReturnType (opens in a new tab) utility type. Accessing nested types would also require the use of bracket notation (opens in a new tab) which is a chore, as opposed to using dot notation (opens in a new tab), which is more convenient.

The following code snippet highlights the differences between using the dot (opens in a new tab) and bracket (opens in a new tab) notations:

// $use as a method on the builder
type Bar = ReturnType<typeof builder.$use>['foo']['bar'];
 
// $use as a property of the builder
type Bar = typeof builder.$use.foo.bar;
//   ^? number

Addendum: Root node

In summary, the root node uniquely returns the register when the $use property is accessed. While the $get method, in contrast, yields the prefixes used in the creation of the builder object when it is called without arguments, and returns a string when it is called with arguments. This behavior diverges from that of other nodes, which generate keys from the nested keys within the register. Given its distinct characteristics, comprehending how the root node works is crucial.

For a more detailed review of the root node, refer to the usage page.