'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
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.
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,
Define
PropValuePairOfXtype PropValuePairOfX<K extends keyof X> = { prop: K; value: X[K] };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} // }At last,
PropValuePairsOfXto extractPropValuePairMapOfX’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(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 |
