Search code examples
typescriptnested-object

How to type nested objects using Object.keys()?


I am using the nested object and trying to get to the properties using Object.keys() and forEach(). The problem is when I want to get to the nested keys filteringState[item][el].

How to type function like this?

interface InitialStateTypes {
    country: {
        "italy": boolean,
    },
    board: {
        "full": boolean,
    },
    transport: {
        "bus": boolean,
        "own": boolean
    },
}

interface FilteringDataTypes {
    country: string[],
    board: string[],
    transport: string[],
}

const collectFilteringData = (filteringState: InitialStateTypes) => {
    let filteringData = <FilteringDataTypes>{};

    Object.keys(filteringState).forEach((item) => {
        Object.keys(filteringState[item]).forEach((el) => {
            if (filteringState[item][el]) {
                if (!filteringData[item]) {
                    filteringData[item] = [el];
                } else {
                    filteringData[item] = [...filteringData[item], el];
                }
            }
        });
    });
    return filteringData;
};


export default collectFilteringData;

Solution

  • It can get a little messy when using the keys method, because it only expects to output strings. (It is not unreasonable since JS Object keys are considered strings, of course TypeScript worries about this more)

    Below is a possible way it could be done:

    interface InitialStateTypes {
      country: {
          "italy": boolean,
      },
      board: {
          "full": boolean,
      },
      transport: {
          "bus": boolean,
          "own": boolean
      },
    }
    
    interface FilteringDataTypes {
      country: string[],
      board: string[],
      transport: string[],
    }
    
    const collectFilteringData = (filteringState: InitialStateTypes):FilteringDataTypes => {
      let filteringData = {} as FilteringDataTypes
    
      (Object.keys(filteringState) as Array<keyof InitialStateTypes>).forEach((item) => {
          (Object.keys(filteringState[item]) as Array<keyof InitialStateTypes[typeof item]>).forEach((el) => {
              if (filteringState[item][el]) {
                  if (!filteringData[item]) {
                      filteringData[item] = [el];
                  } else {
                      filteringData[item] = [...filteringData[item], el];
                  }
              }
          });
      });
      return filteringData;
    };
    
    
    export default collectFilteringData
    
    • Here the type assertion acts to tell TypeScript the types you're actually expecting.
    • It then allows the correct types to be passed through to the forEach method
    • The tricky part is that it's nested, so you have to do another type assertion, but you also want to pass in the value for the typeof item from the first forEach

    Formatted (with Prettier), it ends up looking like this:

    const collectFilteringData = (
      filteringState: InitialStateTypes
    ): FilteringDataTypes => {
      let filteringData = {} as FilteringDataTypes;
    
      (Object.keys(filteringState) as Array<keyof InitialStateTypes>).forEach(
        (item) => {
          (
            Object.keys(filteringState[item]) as Array<
              keyof InitialStateTypes[typeof item]
            >
          ).forEach((el) => {
            if (filteringState[item][el]) {
              if (!filteringData[item]) {
                filteringData[item] = [el];
              } else {
                filteringData[item] = [...filteringData[item], el];
              }
            }
          });
        }
      );
      return filteringData;
    };
    

    Note

    I've written this answer as of TypeScript 4.6.2, the keys method is typed like so:

    keys(o: object): string[];