Typescript and object literal lookups

Minimal reproducible example of the issue

I have an object literal:

const map = (filter: Filter) => {
  return {
    [Filter.USERS]: {
      fetch: async () => getUsers(),
      set: setUsers,
    },
    [Filter.FIRMS]: {
      fetch: async () => getFirms(),
      set: setFirms,
    }
  }[filter];

And I have my state defined like this:

  const [firms, setFirms] = useState<Maybe<FirmsResponse>>(null);
  const [users, setUsers] = useState<Maybe<UsersResponse>>(null);
  const [filter, setFilter] = useState<Filter>(Filter.USERS)

I want to use my object:

const currentFilter = map(filter);
const response = currentFilter.fetch();
currentFilter.set(response.data);
// ^^^^^^^^^^^^^^ Here I get a long TS error which boils down to:
// "Type 'UsersResponse' provides no match for the signature
// '(prevState: Maybe<FirmsResponse>): Maybe<FirmsResponse>'"

Is there a way to set my data like this or am I forced to use something like:

      if (filter === Filter.USERS) {
        setUsers(
          (response.data as unknown) as Users
        );
      } else if ...

Answer

In the interest of having some human-readable IntelliSense, I am going to do some refactoring of your example code. It will not change any of the behavior or the types, aside from implementation details.

First I will annotate your getUsers and getFirms return types as UsersResponse and FirmsResponse:

const getUsers = (): UsersResponse => [{ id: 1, name: "John" }];
const getFirms = (): FirmsResponse => [{ id: 1, firmName: "John's Waffles" }];

Additionally, I’m going to define a FetchSet<T> interface to represent the fetch/set pair for a particular response type T:

interface FetchSet<T> {
  fetch: () => T;
  set: React.Dispatch<React.SetStateAction<Maybe<T>>>
}

And refactor map() to be defined in terms of these types:

const usersFetchSet: FetchSet<UsersResponse> = { fetch: getUsers, set: setUsers };
const firmsFetchSet: FetchSet<FirmsResponse> = { fetch: getFirms, set: setFirms };
const mapper = {
  [Filter.USERS]: usersFetchSet,
  [Filter.FIRMS]: firmsFetchSet
}
const map = (filter: Filter) => {
  return mapper[filter];
};

Okay, so far everything’s the same. Let’s look at your error:

const currentFilter = map(filter); 
// const currentFilter: FetchSet<UsersResponse> | FetchSet<FirmsResponse>

const response = currentFilter.fetch(); 
// const response: UsersResponse | FirmsResponse

currentFilter.set(response); // error!
// Argument of type 'UsersResponse | FirmsResponse' is not 
// assignable to parameter of type 
// '(
//    (FirmsResponse | ((prevState: Maybe<FirmsResponse>) => Maybe<FirmsResponse>)) & 
//    (UsersResponse | ((prevState: Maybe<...>) => Maybe<...>))
//  ) | null'

So the compiler is looking at the input as a union type UsersResponse | FirmsResponse, but it was expecting the input to be an intersection type instead; that is, UsersResponse & FirmsResponse.

The error message goes into a little more detail because the set() methods also accept null and the also accept some callbacks, but you’re not passing in null or callbacks, so we can simplify the error to something like this:

// Argument of type 'UsersResponse | FirmsResponse' is not assignable to 
// parameter of type 'UsersResponse & FirmsResponse'

So, why is this happening, and how can we address it?


One major issue is that the inferred call signature of your map function is this:

// const map: (filter: Filter) => FetchSet<UsersResponse> | FetchSet<FirmsResponse>

No matter what Filter you put in, the compiler only understands that the union is coming out:

const fsOops = map(Filter.USERS);
// const fsOops: FetchSet<UsersResponse> | FetchSet<FirmsResponse>

For all the compiler knows, when you call map(Filter.USERS), a FetchSet<FirmsResponse> will come out. If you want the compiler to know that there is a relationship between the particular Filter you put in and the particular type that comes out, you will need to annotate the call signature appropriately.

In this case you can make map a generic function, where the filter input is of generic type F which is constrained to Filter:

const map = <F extends Filter>(filter: F) => {
  return mapper[filter];
};

/* const map: <F extends Filter>(filter: F) => {
    0: FetchSet<UsersResponse>;
    1: FetchSet<FirmsResponse>;
}[F] */

Now the inferred call signature is generic enough to understand what happens when a particular value goes in:

const fsOkay = map(Filter.USERS);
// const fsOkay: FetchSet<UsersResponse>

So hooray, we’ve solved the problem, right? Let’s see:

const currentFilter = map(filter);
// const currentFilter: FetchSet<UsersResponse> | FetchSet<FirmsResponse>

const response = currentFilter.fetch();
// const response: UsersResponse | FirmsResponse

currentFilter.set(response); // error!
// still Argument of type 'UsersResponse | FirmsResponse' is not assignable to 
// parameter of type 'UsersResponse & FirmsResponse'

Nope, nothing has changed. But why?


The problem is the issue raised in microsoft/TypeScript#30581. The filter value you pass in to map() is a union type; it is Filter.USERS | Filter.FIRMS. And so currentFilter is also a union type: FetchSet<UsersResponse> | FetchSet<FirmsResponse>. The type of response is therefore also a union, UsersResponse | FirmsResponse, and the type of currentFilter.set is also a union, React.Dispatch<React.SetStateAction<Maybe<UsersResponse>>> | React.Dispatch<React.SetStateAction<Maybe<FirmsResponse>>>.

All of this is correct, but there’s a problem. The compiler has no way to model that the currentFilter.set union is correlated with the response union. A human being might evaluate this situation by first imagining that filter is Filter.USERS, and determining the resulting types of currentFilter.set and response, and seeing that they match. And then this human being would imagine that filter is Filter.FIRMS, determine the resulting types of currentFilter.set and Filter.USERS, and seeing that they match, and then conclude that therefore the code block is fine, since it matches for each of the two possible cases.

But the compiler does not analyze the same block of code multiple times this way, and it doesn’t see only two possible cases. It analyzes the block once. currentFilter.set is of two possible types, and response is of two possible types, and therefore the pair of them is of four possible types. For all it can tell from its analaysis, currentFilter.set is a React.Dispatch...<UsersResponse>>> while response is a FirmsResponse. And so it complains.

Since currentFilter.set is a union of functions, before TypeScript 3.3 the compiler would just have given up and said it’s uncallable. TypeScript 3.3 introduced support for calling unions of functions, but it requires that you pass in a parameter which is the intersection of the parameters from each function. The release notes explain why unions-of-functions can only safely take intersections-of-parameters, so I won’t belabor the point here.

Again, the compiler cannot track the correlations between multiple expressions of union types, and so it is unable to verify code as being safe because it is guarding against impossible cross-correlated scenarios. The GitHub issue is mostly just a description of this issue, along with some workarounds and a long list of use cases. This question is another such use case.


So there’s no good support for what you’re doing. The two general workarounds I know of are either to get type safety but at the expense of writing unscalable and redundant code, or to have convenient code but give up some type safety.

The safe-but-redundant code looks like this:

switch (filter) {
  case Filter.FIRMS: {
    const currentFilter = map(filter);
    const response = currentFilter.fetch();
    currentFilter.set(response); // no error
    break;
  }
  case Filter.USERS: {
    const currentFilter = map(filter); // union
    const response = currentFilter.fetch();
    currentFilter.set(response); // no error
    break;
  }
}

Here we are making the compiler do what the human would do: imagine each case for filter and verify that the same code block works in both cases. The compiler is now happy with this. But repeating yourself to appease the compiler is probably only desirable if you value type safety to an extreme degree.

The workaround I usually suggest is to give up some type safety:

currentFilter.set(response as FirmsResponse & UsersResponse); // no error

If the compiler will only accept a FirmsResponse & UsersReponse without complaint, we’ll just assert that response is one of these things. We know it’s not really, but this pretense suppresses the error, and the compiler is happy. If we’re going down this road, we could go all the way and just assert to the any type:

currentFilter.set(response as any); // no error

This is certainly convenient, but whenever you use a type assertion you should be careful that you have not done something unsafe, since the compiler can’t protect you anymore:

// don't do this
const randomFilter = Math.random() < 0.5 ? Filter.FIRMS : Filter.USERS;
const randomResponse = map(randomFilter).fetch()
currentFilter.set(randomResponse as FirmsResponse & UsersResponse); // no error

Here I’m flipping a coin at runtime and passing randomResponse into currentFilter.set. My type assertion has suppressed the error, even though now it really is possible that I’ve passed in the wrong response. So again: be careful not to do something like this.

Playground link to code