John W
John W

Reputation: 199

Object of generic functions

I am trying to get this typescript to work, but I can't work out how to type the openPromises object to have the resolve function not have an error because it's mistyped. The error I am seeing is:

Type '(value?: B | PromiseLike<B> | undefined) => void' is not assignable to type '<C>(value?: C | undefined) => void'.
    Types of parameters 'value' and 'value' are incompatible.
        Type 'C | undefined' is not assignable to type 'B | PromiseLike<B> | undefined'.
            Type 'C' is not assignable to type 'B | PromiseLike<B> | undefined'.
                Type 'C' is not assignable to type 'PromiseLike<B>'.

An easy workaround to this is to type the value of openPromises as any, but was hoping to improve on that and learn a bit more about complex generic types in the process.

Here is the code I have that demonstrates my issue. My source is much more complex, but this is the minimum I could create that demonstrates the issue

interface DefaultResponse {
    a: 'a'
}

const openPromises: {
    [id: string]: { resolve<C>(value?: C): void }
} = {};

function putInObject<B extends DefaultResponse>(id: string) {
    return new Promise<B>(resolve => {
        openPromises[id] = { resolve }; 
    }) 
}

// ----------

interface MyResponse1 extends DefaultResponse {
    AA: string;
}

putInObject<MyResponse1>("test1").then(console.log);

const response1: MyResponse1 = { a:'a', AA: "test" };
openPromises["test1"].resolve(response1)


interface MyResponse2 extends DefaultResponse {
    BB: string;
}

putInObject<MyResponse2>("test2").then(console.log);

const response2: MyResponse2 = { a:'a', BB: "test" };
openPromises["test2"].resolve(response2)

You can see the error in the left side of this typescript code sandbox. Thanks for your help!

Upvotes: 3

Views: 56

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249546

The problem is that what you are trying to do is not really type safe. A generic function is one that has the type argument decided by the caller. In your case, the caller can only pass in a single type that is valid, the type that was decided when putInObject was called for a particular id. So for example this is allowed, even though it's not valid:

putInObject<MyResponse1>("test1").then(r => console.log(r.AA)); // expecting r to be MyResponse1
openPromises["test1"].resolve({ a:'a', BB: "test" }) // I can pas in anything since resolve is generic

There isn't a good way to type openPromises upfront that is truly type safe. Since there is only one valid type for each key and the keys are added later we can't really use a type for openPromise what will emulate this behavior.

There are two workarounds I can think of.

Mutating the type of the original object

Typescript does not allow us to change the type of a variable after it has been declared, we cold however return from putInObject, the same object but with a different type and subsequently use this new object instead. Below, I used a class to keep the current type of openPromisses

class OpenPromisesManager<T = {}> {
  readonly openPromises: T = {} as T;
  putInObject<B extends DefaultResponse>(){
    return <TKey extends string>(id: TKey, withPromise: (p:Promise<B>) => void): OpenPromisesManager<T & Record<TKey, { resolve(value?: B): void }>> =>  {
      const newThis = this as any ;
      withPromise(new Promise<B>(resolve => {
          newThis.openPromises[id] = { resolve }; 
      }));
      return newThis as OpenPromisesManager<T & Record<TKey, { resolve(value?: B): void }>>;
    }
  }
}

// ----------

interface MyResponse1 extends DefaultResponse {
  AA: string;
}
const mgr = new OpenPromisesManager()
const mgr2 = mgr.putInObject<MyResponse1>()("test1", p=> p.then(r=> console.log(r.AA)));

const response1: MyResponse1 = { a:'a', AA: "test" };
mgr2.openPromises["test1"].resolve(response1)
mgr2.openPromises["test1"].resolve({ a:'a', BB: "test" }) // error

Using a key that captures the request type

Another option is to have a key that keeps in its type the expected return type of the promise. This allows us to create a getfunction that will return an object with the apropriate resolve type.

const openPromises: unknown = {};

type Key<T> = string & { __type: T }
function key<T>(k: string) {
  return k as Key<T>;
}
function putInObject<B extends DefaultResponse>(id: Key<B>) {
  return new Promise<B>(resolve => {
    (openPromises as any)[id] = { resolve }; 
  }) 
}

function get<B>(id: Key<B>): { resolve(value?: B): void } {
  return (openPromises as any)[id];
}

// ----------

interface MyResponse1 extends DefaultResponse {
  AA: string;
}

const test1 = key<MyResponse1>("test1");
putInObject<MyResponse1>(test1).then(console.log);

const response1: MyResponse1 = { a:'a', AA: "test" };
get(test1).resolve(response1)
get(test1).resolve({ a: 'a', BB: ''}) // error

Upvotes: 1

Related Questions