Search code examples
typescript

Type 'string' is not assignable to type 'never'. in Array.reduce


I encountered a TypeScript error when using reduce to create an object where the keys and values are the same. Here's my code:

const weather = {
  sunny: '맑음',
  partlyCloudy: '구름 조금',
  mostlyCloudy: '구름 많음',
  cloudy: '흐림',
  rain: '비',
  snow: '눈',
  rainOrSnow: '비 또는 눈',
  snowOrRain: '눈 또는 비',
  thunderstorm: '천둥번개',
  fog: '안개',
  yellowDust: '황사'
};

type WeatherKeys = keyof typeof weather;
type WeatherKeyMap = { [K in WeatherKeys]: K };

const foodKeys = (Object.keys(weather) as WeatherKeys[]).reduce((acc, key) => {
  acc[key] = key;
  return acc;
}, {} as WeatherKeyMap);

I'm receiving the following error on acc[key] = key:

Type 'string' is not assignable to type 'never'.

How can I resolve this error?


Solution

  • You've run into TypeScript's inability to deal with so-called correlated union types as described in microsoft/TypeScript#30581. The line acc[key] = key has key appear twice, where key is of the WeatherKeys union type. TypeScript doesn't check each possible value of key to see if it's valid. Instead, it just treats each utterance of key as essentially independent values of the same union type. It's almost like it's analyzing acc[key1] = key2 where key1 and key2 are both of type WeatherKeys, and complaining that it's not safe.

    The recommended approach in cases like this is to try to refactor to generics in a particular form such that the compiler can be sure that both sides of the assignment are the same. The general approach is detailed in microsoft/TypeScript#47109.

    For this particular example, I'd write it like this:

    const foodKeys = (Object.keys(weather) as WeatherKeys[]).reduce(
      <K extends WeatherKeys>(acc: { [P in K]: P }, key: K) => {
        acc[key] = key;
        return acc;
      }, {} as WeatherKeyMap);
    

    Now the callback is generic, where key is of the generic type K constrained to WeatherKeys, and where acc is of the type {[P in K]: P} which is a supertype of WeatherKeyMap. TypeScript is happy to accept the callback because it matches the expected input type. And inside the callback, TypeScript is happy to accept the assignment acc[key] = key because both sides of the assignment are equivalent to K.

    Playground link to code