Search code examples
typescriptobjectgenericstypeskeyof

Generic type from keyof that is an object


I'd like to type a generic function to only include values that are of type object (and ignore/disallow the other ones).

This is to only allow updating objects within a map. Is it possible? I haven't been able to find a solution.

enum SKey {
  key1 = "key1",
  key2 = "key2",
}

type SItem = {
  [SKey.key1]: boolean;
  [SKey.key2]: {
    attr1: string;
    attr2: boolean;
  };
};

const mapDB: SItem = {
  [SKey.key1]: true,
  [SKey.key2]: {
    attr1: "hello",
    attr2: true,
  },
};

export function updateDbItem<K extends keyof SItem>(
  key: K,
  newFields: Partial<SItem[K]>
) {
  const item: SItem[K] = mapDB[key];
  mapDB[key] = { ...item, ...newFields };
}

But obviously I get the following error

error TS2698: Spread types may only be created from object types.

mapDB[key] = { ...item, ...newFields };
              ~~~~~~~~

Thank you for your help!


Solution

  • You can use a mapped type to derive a union of only the keys whose values are object types.

    In the code you've shown, this evaluates to only SKey.key2, but if you add more entries to SItem, then it will also include other keys that meet the criteria.

    Here's an example:

    TS Playground

    type SItemObjectKey = keyof {
      [
        K in keyof SItem as SItem[K] extends Record<string, unknown>
          ? K
          : never
      ]: unknown;
    };
    
    export function updateDbItem<K extends SItemObjectKey>(
      key: K,
      newFields: Partial<SItem[K]>
    ) {
      const item: SItem[K] = mapDB[key];
      mapDB[key] = { ...item, ...newFields }; // Ok now!
    }
    

    See also: the utility type Record<Keys, Type>