0

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:

  1. What is the above error message really saying?
  2. 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?
3
  • stackoverflow.com/a/56701587/135978 may help somewhat. Commented Nov 27, 2021 at 17:01
  • Not really a good answer, but I'd be inclined to just 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. Commented Nov 27, 2021 at 21:24
  • @AlexWayne Yeah, I thought about it some more and I have to agree with you. I think the question I'm asking doesn't make much sense because if you have an array of mixed objects, there's no way to "pin" individual objects to the types that were used to originally create those objects. What you're proposing makes sense. Commented Nov 29, 2021 at 18:00

1 Answer 1

1

I find it hard to explain what's happening, but hopefully an example will help.

Example

Definition of EveryKey and Key

Let's assume that EveryKey is 1 | 2 | 3.
Key extends EveryKey means that:

  • 1 | 2 is accepted
  • 1 | 2 | 3 | 4 is not.

Definition of Result

Your Result<Key> is basically saying you expect an object that has a key for every number in Key.

So in this case the types would be equivalent to...

  • EveryKey
    { 
      1: string; 
      2: string; 
      3: string;
    }
    
  • Key
    { 
      1: string; 
      2: string; 
    }
    

The error

If you take the 2 interfaces above, then typescript is saying that you cannot put the second type into an array of the first, since it doesn't have a 3 key.

Admittedly, it's saying it in a way that is very hard to read/understand.

The solution

I still find it hard to understand what you'll use this for, however it feels like your dictionary should make use of Partial to indicate that the keys are optional.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks. In the end I decided not to go with this solution as I realized that if you have an array of objects where each object is of a different type, it's impossible to gain access to the types of those objects. The array must have an overarching type composed of the types of each item in that array. That said, your answer is still helpful to know for people who are trying to google this error message, so I will accept it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.