'Typescript: Reference type of property by other property of same object

See the following code:

interface X {
    a: string;
    b: number;
    c: boolean;
}

type A = {
    prop: keyof X;
    value: X[keyof X];
}

const a: A = { prop: 'a', value: 'a' }; // ok
const b: A = { prop: 'a', value: 1 }; // not ok, value should be string, because X['a'] is string
const c: A = { prop: 'b', value: 1 }; // ok
const d: A = { prop: 'c', value: 1 }; // not ok, value should be boolean, because X['c'] is string

Here I want the type of the .value property to be string, if prop is "a", number for "b" and boolean for "c", but instead it is string|number|boolean for all cases, because keyof X can refer to different keys for each use for type A. How can I make it refer to the same property twice, without having it explicitly input it into a generic argument of A?

I feel like I should be using infer her, but I'm not sure how, and also I might be on the wrong track there.



Solution 1:[1]

I don't believe you can do this without generics. So here it is:

type A<K extends keyof X> = {
    prop: K;
    value: X[K];
}

The good news is, you only need to specify the generic argument when you're assigning to a variable of an explicitly given type, like so:

const a: A<'a'> = { prop: 'a', value: 'a' };

And it's possible that you will never have this need in your code. For example if you specify a function using this generic type, you will not need to explicitly specify the generic parameter to get it working:

function fn<K extends keyof X>(a: A<K>) {
    // something
}

fn({ prop: 'a', value: 'a' }); // ok
fn({ prop: 'a', value: 1 }); // not ok, value should be string, because X['a'] is string
fn({ prop: 'b', value: 1 }); // ok
fn({ prop: 'c', value: 1 }); // not ok, value should be boolean, because X['c'] is string

Playground link

Solution 2:[2]

You want A to be a union of {prop: K; value: X[K]} for each K in keyof X, like this:

type A = {
    prop: "a";
    value: string;
} | {
    prop: "b";
    value: number;
} | {
    prop: "c";
    value: boolean;
};

In each element of that union, there is a correlation between the prop type and the value type, which prohibits you from assigning prop and value types from different members of X:

const a: A = { prop: 'a', value: 'a' }; // ok
const b: A = { prop: 'a', value: 1 }; // error
const c: A = { prop: 'b', value: 1 }; // ok
const d: A = { prop: 'c', value: 1 }; // error

You can also make the compiler calculate this for you programmatically in a few ways, such as building a mapped type that you immediately index into:

type A = { [K in keyof X]-?: {
    prop: K;
    value: X[K];
} }[keyof X];

It can be verified via IntelliSense that the above definitions of A are equivalent, but now A will update automatically if you modify X.

Playground link to code

Solution 3:[3]

Your final goal of A is this type. (the same as first answer)

type A = {
    prop: "a";
    value: string;
} | {
    prop: "b";
    value: number;
} | {
    prop: "c";
    value: boolean;
};

To make this type,

  1. Define PropValuePairOfX

    type PropValuePairOfX<K extends keyof X> = { prop: K; value: X[K] };
    
  2. Define PropValuePairMapOfX.

    type PropValuePairMapOfX = { [K in keyof X]: PropValuePairOfX<K> };
    // {
    //   a: {prop: 'a', value: string},
    //   b: {prop: 'b', value: number},
    //   c: {prop: 'c', value: boolean}
    // }
    
  3. At last, PropValuePairsOfX to extract PropValuePairMapOfX ’s values as UnionType.

    // This type is the same as A
    type PropValuePairsOfX = PropValuePairMap[keyof X];
    //  {prop: 'a', value: string} | {prop: 'b', value: number} | {prop: 'c', value: boolean}
    //  This type is the same as our goal type ?
    
    const a: PropValuePairsOfX = { prop: 'a', value: 'a' }; // ok
    const b: PropValuePairsOfX = { prop: 'a', value: 1 }; // not ok, value should be string, because X['a'] is string
    const c: PropValuePairsOfX = { prop: 'b', value: 1 }; // ok
    const d: PropValuePairsOfX = { prop: 'c', value: 1 }; // not ok, value should be boolean, because X['c'] is string
    
  4. (Option) By making X generic argument type, we can make more general type.

    interface X {
      a: string;
      b: number;
      c: boolean;
    }
    
    type PropValuePair<T, K extends keyof T> = { prop: K; value: T[K] };
    type PropValuePairMap<T> = { [K in keyof T]: PropValuePair<T, K> };
    type PropValuePairsUnion<T> = PropValuePairMap<T>[keyof T];
    
    const validA: PropValuePairsUnion<X> = { prop: 'a', value: 'a' }; // ok
    const invalidA: PropValuePairsUnion<X> = { prop: 'a', value: 0 }; // ng
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Vojt?ch Strnad
Solution 2 jcalz
Solution 3 kazuwombat