Search code examples
javascriptreactjstypescripttypeerror

What type should I declare for my data (using TypeScript)?


This is part of my TypeScript code.

enum FirstEnum {
    firstEnumKey1 = 'firstEnumKey1',
    firstEnumKey2 = 'firstEnumKey2',
    firstEnumKey3 = 'firstEnumKey3',
    firstEnumKey4 = 'firstEnumKey4',
}

enum SecondEnum {
    secondEnumKey1 = 'secondEnumKey1',
    secondEnumKey2 = 'secondEnumKey2',
    secondEnumKey3 = 'secondEnumKey3',
}

// map first enum to second enum
const map: {[i in FirstEnum]: SecondEnum[] } = {
    'firstEnumKey1': [SecondEnum.secondEnumKey1],
    'firstEnumKey2': [],
    'firstEnumKey3': [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
    'firstEnumKey4': [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
};

I have two enums and a const which map them. I'm using the map to define some data that for each SecondEnum has an object. The object keys should be map keys which had that specific SecondEnum value in their array. So now my result should be like this.

const getSecondEnumValuesBasedOnMap = {
    [SecondEnum.secondEnumKey1]: {
        [FirstEnum.firstEnumKey1]: 'some value',
    },
    [SecondEnum.secondEnumKey2]: {
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
    },
    [SecondEnum.secondEnumKey3]: {
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
    },
}

If I change my map my data should change based on it. for example if I add SecondEnum.secondEnumKey2 to FirstEnum.firstEnumKey2 my data should change like this:

// map first enum to second enum
const map: {[i in FirstEnum]: SecondEnum[] } = {
    'firstEnumKey1': [SecondEnum.secondEnumKey1],
    'firstEnumKey2': [SecondEnum.secondEnumKey2],
    'firstEnumKey3': [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
    'firstEnumKey4': [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
};

const getSecondEnumValuesBasedOnMap = {
    [SecondEnum.secondEnumKey1]: {
        [FirstEnum.firstEnumKey1]: 'some value',
    },
    [SecondEnum.secondEnumKey2]: {
        [FirstEnum.firstEnumKey2]: 'some value',
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
    },
    [SecondEnum.secondEnumKey3]: {
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
    },
}

I want to declare a type for my result so when I changed my map I get errors to update my result but I'm kind of new to typescript and I don't know how.


Solution

  • First of all, we need to change the type of the map, since it is currently not having literal values for the SecondEnum and we won't be able to determine which exact values are used.

    To prevent the compiler from widening the types we will use const assertion and to maintain type safety we will use satisfies operator:

    const map = {
      firstEnumKey1: [SecondEnum.secondEnumKey1],
      firstEnumKey2: [SecondEnum.secondEnumKey2],
      firstEnumKey3: [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
      firstEnumKey4: [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3],
    } as const satisfies {[K in FirstEnum]: readonly SecondEnum[]};
    
    // type EnumMap = {
    //   readonly firstEnumKey1: readonly [SecondEnum.secondEnumKey1];
    //   readonly firstEnumKey2: readonly [SecondEnum.secondEnumKey2];
    //   readonly firstEnumKey3: readonly [SecondEnum.secondEnumKey2, SecondEnum.secondEnumKey3];
    //   readonly firstEnumKey4: readonly [...];
    // }
    type EnumMap = typeof map;
    

    Next, let's define a type that accepts a generic parameter constrained by SecondEnum and find the members of FirstEnum that have this specific item in the map. We are going to use mapped types to map through the map and key remapping to exclude those that are not suitable, and at the end return the keys of the map that have this second enum in them:

    type FindValue<T extends SecondEnum> = keyof  {
      [K in keyof EnumMap as T extends  EnumMap[K][number] ? K : never]:EnumMap[K]
    }
    

    Let's define a type for the getSecondEnumValuesBasedOnMap. We will need to use mapped types and key remapping again to exclude those second enum members that are not used at all:

    {
      [K in SecondEnum as [FindValue<K>] extends [never] ? never : K]: {
        [P in FindValue<K>]: string
      }
    }
    

    Note that [] are really important in the [FindValue<K>] extends [never], since never is an empty set and is a supertype for every union and to make sure that this check works as expected we prevent the compiler from distributing the types using the []. And in the values, we are mapping through the result of FindValue<K> which returns as the keys where the second enum is used and assigns a value of type string:

    const getSecondEnumValuesBasedOnMap:{
      [K in SecondEnum as [FindValue<K>] extends [never] ? never : K]: {
        [P in FindValue<K>]: string
      }
    } = {
      [SecondEnum.secondEnumKey1]: {
        [FirstEnum.firstEnumKey1]: 'some value',
      },
      [SecondEnum.secondEnumKey2]: {
        [FirstEnum.firstEnumKey2]: 'some value',
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
      },
      [SecondEnum.secondEnumKey3]: {
        [FirstEnum.firstEnumKey3]: 'some value',
        [FirstEnum.firstEnumKey4]: 'some value',
      },
    };
    

    playground