I have a class with a method that builds an object where the keys of that object are known in advance — thus the object should not allow access of any keys but those specified — and then pushes that object onto an array held by that class. The way I've chosen to constraint the object is by having the method take a generic type which is a union of the valid keys. The thing is that upon calling the method in question, different sets of keys can be specified, therefore the objects built may have different keys in them. Because I'm building an array, I'm trying to figure out what the type of the resulting array should be. So far I've come up with something like this:
type Result<Key extends number> = Record<Key, string>;
function buildResult<Key extends number>(keys: readonly Key[]): Result<Key> {
return keys.reduce((obj, key) => {
return { ...obj, [key]: 'whatever' };
}, {} as Result<Key>);
}
class RequestPool<EveryKey extends number> {
private results: Result<EveryKey>[];
constructor() {
this.results = [];
}
fetch<Key extends EveryKey>(keys: readonly Key[]): Result<Key> {
const result = buildResult(keys);
this.results.push(result);
return result;
}
}
Here is an example of how the above code could be used:
const requestPool = new RequestPool<1 | 2 | 3 | 4 | 5>();
requestPool.fetch([1, 2, 3]);
requestPool.fetch([4, 5]);
requestPool.fetch([]);
Note that the developer would need to specify EveryKey up front, but nothing else when calling fetch as Key should be inferred based on the keys given. Also, as mentioned, the objects are constrained by the keys. For instance:
const result = requestPool.fetch([1, 2, 3]);
result[1] // should work
result.foo // should not work
Here's the deal: this doesn't quite work. Namely, in this method:
fetch<Key extends EveryKey>(keys: readonly Key[]): Result<Key> {
const result = buildResult(keys);
this.results.push(result);
return result;
}
the this.results.push(result); line is failing with the following error:
Argument of type 'Result<Key>' is not assignable to parameter of type 'Result<EveryKey>'.
Type 'EveryKey' is not assignable to type 'Key'.
'EveryKey' is assignable to the constraint of type 'Key', but 'Key' could be instantiated with a different subtype of constraint 'number'. (2345)
I have a theory as to what could be causing the issue and have attempted to "fix" this by declaring results in the class as (Result<EveryKey> | Result<never>)[] instead of just Result<EveryKey>. That works for this example, but it does not scale -- for instance, if we want our buildResult function to return something more complicated such as:
function buildResult<Key extends number>(keys: readonly Key[]) {
const response = keys.reduce((obj, key) => {
return { ...obj, [key]: 'whatever' }
}, {});
return { response };
}
I am not sure why I have to make the above change, anyway. Assuming the issue is around never, if EveryKey is never, then Key should be never as well, because otherwise this wouldn't make any sense:
const requestPool = new RequestPool<never>();
requestPool.fetch([1, 2, 3]);
Here is the code above on TypeScript Playground.
What I want to know is:
- What is the above error message really saying?
- Is there any other way to assign a type to
results? Is there another way to enforce an object in the manner I've described?
this.results.push(result as Result<EveryKey>);in this case. That is, if you are sure that the logic is sound and that is indeed typesafe.