Reputation: 199
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
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.
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
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 get
function 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