Can a class property that necessitates type arguments be assigned in a lazy manner?

I find myself in a bit of a dilemma.

The class I have is within my control for modifications, but it needs to be dynamically instantiated by an external code that I have no authority over. Unfortunately, this code cannot pass generic type arguments to the constructor, which necessitates that the constructor arguments must strictly be plain data (such as JSON serializable). Furthermore, this class contains a property that requires an instance of another class (that I do not have any influence on) with a generic type argument that is a subset of a union type and function arguments which I would prefer to lazily instantiate due to these restrictions.

In essence, I am tasked with acting as a liaison between two disparate pieces of unmodifiable code.

Here's a simplified example:

type ABC = 'a' | 'b' | 'c';

// Foo is beyond my control.
class Foo<T extends ABC> {
  foo (x: T): void {
    console.log(x.repeat(2))
  }
}

// Bar itself cannot be generic, as explained in the scenario.
// While I can tweak the implementation of Bar, Foo or the code
// instantiating Bar remain off-limits.
class Bar {
  foo?: Foo; // <-- Issue lies here

  // I retain control over the code calling this method
  createFoo<T extends ABC>(): void {
    this.foo = new Foo<T>()
  }
}

When used independently, Foo performs as expected:

const test1 = new Foo<'b' | 'c'>();
test1.foo('a'); // appropriately errors out

I attempted to utilize the union itself as a placeholder...

class Baz {
  foo?: Foo<ABC>

  createFoo<T extends ABC>(): void {
    this.foo = new Foo<T>()
  }
}

However, this led to:

const bz = new Baz();
bz.createFoo<'b' | 'c'>();
bz.foo?.foo('a') // Oops! This should fail just like in the initial example.

Is there a viable way to resolve this conundrum? I acknowledge these constraints are quite peculiar, but unfortunately, they are non-negotiable.

Playground

Answer №1

It is impossible to alter types that have already been instantiated without creating a new type. In this scenario, Bar/Baz will adhere to their instantiated type, and it cannot be modified from within the class.

To work around this limitation, you can create a new type and return that instead. This can be achieved through either a static method or a regular method. By cleverly using the unknown type, you can inform users about the specific way to set foo.

You could implement something like the following code snippet, which merges types:

class Bar {
  foo?: unknown;

  static createFoo<T extends ABC>(bar: InstanceType<typeof Bar>, foo: Foo<T>) {
    bar.foo = foo
    return bar as InstanceType<typeof Bar> & {foo: Foo<T>}
  }
}

const bar1 = new Bar();
const bar2 = Bar.createFoo(bar1, new Foo<'b' | 'c'>())
bar1.foo() //incorrect usage errors entirely
bar2.foo('a') //errors as expected

If you prefer not to pass Foo as a parameter, you can accomplish it like this:

class Bang {
  foo?: unknown

  static createFoo<T extends ABC>(fw: InstanceType<typeof Bang>) {
    fw.foo = new Foo()
    return fw as InstanceType<typeof Bang> & {foo: Foo<T>['foo']} // use ['foo'] index accessor since there is no constructor
  }
}

const bang1 = new Bang()
const bang2 = Bang.createFoo<'b'>(bang1)
bang1.foo('a') //error! type is unknown
bang2.foo('a') //error! cant use a

Alternatively, you can achieve the same outcome with a regular method:

class Fubar {
  foo?: unknown

  createFoo<T extends ABC>() {
    this.foo = new Foo()
    return this as InstanceType<typeof Fubar> & {foo: Foo<T>['foo']} // use ['foo'] index accessor since there is no constructor
  }
}

const fubar1 = new Fubar()
const fubar2 = fubar1.createFoo<'b'>()
fubar1.foo('a') //error! type is unknown
fubar2.foo('a') //error! cant use a

TS Playground

As a side note: this challenge stems from TypeScript's design philosophy. If you do not control the instantiation of Bar/Baz, how can you ensure that it instantiates Foo as intended? In the provided example, there are no discernible runtime differences between these functions, making it more challenging. One possible solution is utilizing type predicates to discriminate based on runtime values and create control flow.

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

Steps for transitioning a VUE JS project to TypeScript

Is it possible to transition a VUE JS project from JavaScript to TypeScript without rewriting everything? I heard from a friend that it can be done through the VUE CLI, but I haven't been able to find any documentation or articles on this method. Has ...

Exploring Angular component testing through jasmine/karma and utilizing the spyOn method

I have been facing an issue while trying to test my component. Even though the component itself works perfectly, the test keeps generating error messages that I am unable to resolve. Here is the snippet of code that I am attempting to test: export cl ...

Static methods utilizing generics

Why does this function work without any exceptions? Even though T must be the same in this case, one is a String and the other is an ArrayList<Integer>. public static void main(String[] args) { Serializable s = pick("d", new ArrayList<Integ ...

How can you prevent form error messages from displaying with the autofocus directive when navigating to a new component?

I'm currently working on an authentication application utilizing the PEAN stack - PostgreSQL, ExpressJS, Angular, and NodeJS. Within my project, I've implemented an autofocus directive: autofocus.directive.ts import { Directive, ElementRef } fr ...

Tips for obtaining intellisense for the value in a readonly string array property within an object without relying on generics

Keeping it brief and to the point. What is the reason behind this? type ReadonlyStringArray = readonly string[] type TEnumGeneric<EnumIsh extends ReadonlyStringArray> = { type: "enum", values: EnumIsh } type TEnumImplied = { t ...

Establish a table containing rows derived from an object

I am currently dealing with a challenge in creating a table that contains an array of nested objects. The array I have follows this schema: array = [ { id: 'Column1', rows: { row1: 'A', row2 ...

What is the best way to pass createdDt and updatedDat values in an Angular form without displaying them on the template?

I am working on a message creation form in Angular where I need to include createdDt and updatedDt as hidden values. These values should not be visible in the template. I want to automatically set the current date and time for both createdDt and updatedD ...

A TypeScript interface or class

Just had a lively discussion with a coworker and wanted to get some clarification. When shaping an object in TS, should we use a class or an interface? If I need to ensure that a function returns an object of a specific type, which one is the best choice? ...

Utilizing Typescript generics with an optional second parameter

Learning about generics in typescript has been quite challenging for me. However, I was able to make it work successfully. export type Events = { LOGIN: undefined NAVIGATION: { screen: string } SUPPORT: { communication_method: 'chat&ap ...

Unlocking the secrets of integrating Vuex store with JavaScript/TypeScript modules: A comprehensive guide

I am working on a vue application and I have a query. How can I access the store from javascript/typescript modules files using import/export? For example, if I create an auth-module that exports state, actions, mutations: export const auth = { namesp ...

Having trouble iterating over an array in Angular's TypeScript

I've been attempting to loop through an array of objects in TypeScript, but encountered the following error: ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'forEach' of undefined TypeError: Cannot read property 'fo ...

"Using an indexer in TypeScript allows for direct access to object properties by simply specifying the key within

I have a requirement to access an object property using a string as the key interface MyObject { prop1: string; prop2: string; prop3: string; prop4: string; prop5: string; } let initialValues: MyObject; //I initialize some properties initialVa ...

Having trouble sending posts to an API route with Angular's HTTP module

When attempting to send values via a POST request in the URL of an Angular5 Laravel API route, I encountered an issue. Despite not receiving any errors in the browser console, no network activity was recorded upon sending the request. It is perplexing as I ...

Minimize overlap across both projects

One scenario is having two projects that utilize a lot of the same components. How can we minimize redundancy between them? Is there a way to make them shareable? Perhaps leveraging webpack could be the solution? ...

What causes the canLoad function to create an endless loop when rerouting?

Utilizing Angular's canLoad function in my AuthGuard to authenticate a lazy loaded module at the root of my application. If the user is not authenticated, the module will not load and the user will be directed to the login page. This process works sm ...

Dealing with errors in Angular when using Firebase and observables can

I'm facing an issue with implementing error detection in Angular Firebase. Whenever new users log in to my website, they encounter a permission error in the database, which is displayed in the console. My goal is to have auth.isApproved return false ...

Node appears to be struggling to find the cors

I added the cors package and confirmed that it's inside the node_modules directory. However, I keep encountering this error message. /usr/src/app/node_modules/ts-node/src/index.ts:859 server | return new TSError(diagnosticText, diagnosticCodes, ...

The problem with the "typescript-react-apollo" codegen plugin is that it is declaring block-scope variables more than once in the generated

Recently, I embarked on a new project utilizing NextJS with graphql-codegen to auto-generate my apollo-react types from the API. Unfortunately, it seems to be generating duplicate exported variables right from the start, causing a typescript error and hi ...

Angular - The argument provided is not compatible with the parameter

I encountered the following TypeScript errors in app.component.ts: Issue: Argument of type '(events: Event[]) => void' is not assignable to parameter of type '(value: Event[]) => void'. Description: Types of parameters 'e ...

Tips for identifying and handling errors in Playwright

I'm going crazy trying to handle a TimeoutError that I know is coming. Currently, I'm testing the Hidden Layers scenario from UI Testing Playground in Playwright Node.js and I want to see if there's a way to prevent the TimeoutError from cau ...