Monitoring Data Types in TypeScript for Accurate Code Interpretation

I have successfully implemented a Builder design pattern in TypeScript for certain entities. One of these implementations is shown below (simplified), and can also be accessed in the playground:

type Shape = any;
type Slide = any;
type Animation = any;

export class SlideBuilder {

  private slide: Slide;

  public static start() { return new SlideBuilder(); }

  public withShape(name: string, shape: Shape): this {
    this.slide.addShape(name, shape);
    return this;
  }

  public withAnimation(name: string, animation: Animation): this {
    this.slide.addAnimation(name, animation);
    return this;
  }

  public withOrder(shape: string, animations: string[]) {
    this.slide.addOrder(shape, animations);
    return this;
  }
}

SlideBuilder
  .start()
  .withShape("Hello World", {})
  .withAnimation("Animation1", {})
  .withAnimation("Animation2", {})
  .withOrder("Could be 'Hello World' only", ["Could be 'Animation1' or 'Animation2' only"])

The challenge I am facing now is to enforce type checking to ensure that the withOrder method is called with correct parameters that were previously passed to withShape or withAnimation.

I attempted to introduce generic types to the class like so:

export class SlideBuilder<S, A> {
  withShape(name: S, shape: Shape)
  withAnimation(name: A, animation: Animation)
  withOrder(shape: S, animation: A[])
}

However, I could not figure out how to keep track of each call, such as collecting the types from individual calls into a union type. It seems necessary to somehow specify

withOrder(shape: S1 | S2 | S3 | ... | Sn)
, where Sn is a type from the withShape call. If you have any insight on how to effectively implement this, please share your thoughts.

Answer №1

This question has been a joy to tackle!

Is it feasible for the compiler to keep track of all arguments received by a class instance's methods throughout its lifespan?

What a challenging task! Initially, I wasn't sure if it could be done.

Here is what the compiler needs to do over the duration of the class instance:

  • Aggregate all received arguments on each method call.
  • Categorize these arguments for future type checking purposes.

Let's dive in...

Solution

The approach outlined below focuses solely on method signatures, keeping them minimalistic to convey the concept effectively. Implementing the methods should be relatively straightforward.

This strategy utilizes accumulator types to monitor argument types. These accumulators resemble those used in an Array.reduce function.

Refer to the playground link and the code snippets provided:

type TrackShapes<TSlideBuilder, TNextShape> = 
  TSlideBuilder extends SlideBuilder<infer TShapes, infer TAnimations> 
  ? SlideBuilder<TShapes | TNextShape, TAnimations> 
  : never;

// Repeat similar pattern for animations...

export class SlideBuilder<TShape, TAnimation> {
    // Method implementations go here...
}

Understanding the Process

We define two accumulator types for the SlideBuilder, which extract shape and animation types from an existing instance, widen the appropriate generic type using a union, and return the modified SlideBuilder. This is the crux of the solution.

The start method initializes the SlideBuilder with 'never', essentially setting up the base state. Utilizing 'never' ensures that the union operation preserves the existing type (equivalent to adding zero).

Each invocation of withShape and withAnimation adjusts the return type based on the respective accumulator, broadening the type and organizing the argument into the correct category!

Note that the generics in withShape and withAnimation are constrained to strings, preventing unnecessary type widening and offering users a more intuitive interface without requiring as const.

The end result? Efficient tracking of argument types! The test cases below demonstrate how this fulfills the specified requirements.

Test Cases

// Passes type checking.
SlideBuilder
  .start()
  .withShape("Shape1")
  .withAnimation('Animation1')
  .withOrder("Shape1", ["Animation1"])

// Additional test cases here...

Evolution of the Approach

To explore the development journey of this solution, check out these progressive iterations:

Playground Link Initial implementation supporting shapes with as const.

Playground Link Enhancing the class with animations, eliminating the need for as const.

Playground Link Almost final version achieving the desired outcome.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using curl to send a POST request with the specified MIME type

Recently, I've been exploring the possibilities of utilizing an API known as Sky Biometry for face detection and facial recognition purposes. This API allows users to submit either a URL or a POST request in MIME type format. In my case, I'm inte ...

What is the process for accessing my PayPal Sandbox account?

I'm having trouble logging into my SandBox Account since they updated the menu. The old steps mentioned in this post Can't login to paypal sandbox no longer seem to work. Could someone please provide me with detailed, step-by-step instructions o ...

The message shown on items.map stating that parameter 'item' is implicitly assigned the type 'any'

Currently, I am delving into the world of Ionic React with Typescript by developing a basic app for personal use. My current challenge involves dynamically populating an IonSegment from an array. Here is the relevant code snippet: const [items, setItems] ...

What is the best way to sort through this complex array of nested objects in Typescript/Angular?

tableData consists of an array containing PDO objects. Each PDO object may have zero or more advocacy (pdo_advocacies), and each advocacy can contain zero or more programs (pdo_programs). For example: // Array of PDO object [ { id: 1, ...

When transmitting information to the server, the browser initiates four requests

I am encountering an issue with my React component. The problem arises when I try to retrieve the current geographic coordinates, as they are being fetched 4 times consecutively. This same glitch occurs when attempting to send the coordinates to the serv ...

Creating an object instance in Angular 2 using TypeScript

Looking for guidance on creating a new instance in TypeScript. I seem to have everything set up correctly, but encountering an issue. export class User implements IUser { public id: number; public username: string; public firstname: string; ...

Is the performance impacted by using try / catch instead of the `.catch` observable operator when handling XHR requests?

Recently, I encountered an interesting scenario. While evaluating a new project and reviewing the codebase, I noticed that all HTTP requests within the service files were enclosed in a JavaScript try / catch block instead of utilizing the .catch observable ...

Interactive form control for location details including country, state, district, and town

I am struggling with adding dynamic form controls on dropdown change. I have been able to add them, but encountered an error preventing me from retrieving the value in 'formName.value'. The specific error message states: "Error: There is no Form ...

Navigating the parent scope in Angular using TypeScript

Is there a way to access the parent Controller's scope from within the TypeScript class? Here is the code snippet: export class EntityOverviewCtrl extends AbstractCtrl { public static $inject = ["$state", "$http", "CurrentSession"]; publi ...

Create a number line dynamically with Typescript

I am currently working on a project where users can create sales. Within each sale, users are able to add multiple products which are stored in an array and then submitted to a table. I need each product row in the table to have a unique ID known as "line_ ...

Using Typescript and react-redux with a Stateful Component: The parameter type 'typeof MyClass' does not match the expected component type

I've been trying to dive into the world of Typescript, React, and Redux. However, I've hit a roadblock at the moment. This is the error I encountered: ./src/containers/Hello.tsx [tsl] ERROR in /home/marc/Development/TypeScript-React-Starte ...

Modify the color of the select element when it is in an open state

If you're new to Material UI, I have a select element that I would like to change the color of. When 'None' is selected, I want the background color of the input field above it to match the 'None' section. Then, when the dropdown m ...

Discovering the versatility of Typescript objects

I want to define a type that follows this rule: If the property container is present, then expect the property a. If the property item is present, then expect the property b. Both container and item cannot exist at the same time. The code I would expect ...

Error message when attempting to call an invalid hook in a React App following the reinstallation of node modules

Everything was running smoothly on my app until I decided to create a build for testing purposes, which resulted in the following error: Minified React error #321; Error: Minified React error #321; Oddly enough, the app was functioning properly on localho ...

Bidirectional data binding with Observable object

I have been trying to connect isSelected to an object wrapped in an observable. When I attempted this without the observable, it worked perfectly fine. However, within my component, I am facing an issue where measure.isSelected always returns false, even w ...

From transitioning from AngularJS to the latest version Angular 8

I have an Angular application where I need to update some old AngularJS code to work with Angular table.html <table ngFor="let group of vm.groups" style="float: left"> <thead> <tr> <th><b>Sl. No</b ...

Error encountered while building Ionic2 Typescript device: Uglifyjs failed to minify due to SyntaxError, where an unexpected token operator was found '=&', expected punctuation ','

Looking to incorporate a 360 video player into my Ionic app. Here's the code I have so far: HTML: <ion-content> <video id="videojs-panorama-player" class="video-js vjs-default-skin" src="" crossorigin="anonymous" controls ref="player"& ...

EventListener cannot be removed

My TypeScript class is structured like this: class MyClass { let canvas: any; constructor(canvas: any) { this.canvas = canvas; this.canvas.requestPointerLock = this.canvas.requestPointerLock; document.exitPointerLock = ...

Achieve cleaner code by learning to utilize pipes in fp-ts for conditional statement removal

Exploring the use of pipe and Option in fp-ts for my initial encounter. I encountered this code snippet that performs type narrowing, but I believe it can be achieved without using the if statement: if (O.isNone(this.state)) { return undefined; } retur ...

Angular: controller's property has not been initialized

My small controller is responsible for binding a model to a UI and managing the UI popup using semantic principles (instructs semantic on when to display/hide the popup). export class MyController implements IController { popup: any | undefined onShow(con ...