Search code examples
typescripttypescript-genericstype-inference

typescript doesn't infer type of value for a key


what's wrong with the way I define this type

type Payload<Es> = {
  trigger:
    Es extends Record<infer K, infer V>
     ? { key: K, data: V } : never
}

type Evs = {
  "item.1": { ok: "Y", code: number }
  "item.2": 1 | 0
}
const payload: Payload<Evs> = {
  trigger: {
    key: "item.1",
    data: 1
  }
}

payload.trigger.data doesn't get inferred as I expect

expected

{ ok: "Y", code: number }

what I get

0 | { ok: "Y", code: number } | 1


Solution

  • The Record<K, V> utility type is a type corresponding to objects with property keys of type K and property values of type V. But specific keys are not associate with specific values. If you start with {a: string, b: number} and try to infer a Record<K, V> from it, you'll get Record<"a" | "b", string | number>. That's the same type you'd get from {a: number, b: string}. So you really don't want to infer to a Record at all here, unless you want to mix all the properties together.

    Instead you really need to just map over the type, iterating over each key K, and producing the corresponding object type, and then index into the mapped type to get a union of properties. That is, you want a distributive object type (as coined in microsoft/TypeScript#47109):

    type Payload<E> = {
      trigger: { [K in keyof E]: { key: K, data: E[K] } }[keyof E]
    }
    

    You can verify that this gives you what you're looking for:

    type Evs = {
      "item.1": { ok: "Y", code: number }
      "item.2": 1 | 0
    }
    
    type PEvs = Payload<Evs>;
    /* type PEvs = {
        trigger: {
            key: "item.1";
            data: {
                ok: "Y";
                code: number;
            };
        } | {
            key: "item.2";
            data: 0 | 1;
        };
    } */
    
    const payload: Payload<Evs> = {
      trigger: {
        key: "item.1",
        data: 1 // error!
      }
    }
    

    Playground link to code