Reputation: 203
I have a function which accepts 3 parameters: data
, changedProp
and newPropValue
.
The changedProp
should be bound to the newPropValue
parameter and both have a relation to the data parameter.
Let's say we have the Data
type:
interface Data {
a: number,
b: { x: number, y: number}
}
changedProp
is "a"
the newPropValue
should be number
.changedProp
is "b"
the newPropValue
should be { x: number, y: number }
.Why in the example below the newPropValue
in the if statement is not of type { x: number, y: number }
? How to achieve it?
interface Data {
a: number,
b: { x: number, y: number}
}
export function getChangedDataByProperty<T extends Data>(
data: T,
changedProp: keyof T,
newPropValue: T[keyof T]
): T {
if (changedProp === "b") {
return {
...data,
b: { // Error
x: newPropValue.x, // why ts is not smart enough to detect that newPropValue is { x: number, y: number}?
y: newPropValue.y,
}
}
} else {
return {
...data,
x: newPropValue
}
}
}
Here is the TS playground.
Upvotes: 1
Views: 82
Reputation: 23835
As already discussed in the comments, there is currently no relation between changedProp
and newPropValue
. It's not some compiler limitation. You typed changedProp
as keyof T
, which is a union of all the keys, and newPropValue
as T[keyof T]
which is a union of all the value types.
To create a relation between both parameters, you could add a generic type which holds the type of changedProp
and use it to derive the type of newPropValue
as @jonrsharpe demonstrated. This would work for the caller of the function. But you would get assignability errors in the implementation. The compiler is currently only able to discriminate discriminated unions. But two types which depend on a generic type are not a discriminated union; control flow analysis will therefore not do any narrowing.
We need to build a discriminated union to make the compiler understand that checking the value of changedProp
changes the type of newPropValue
.
To keep the calling signature of the function as is, we make use of rest parameters. This will allow us to use a union of tuples to describe possible combinations of parameters.
...[changedProp, newPropValue]: {
[K in keyof Data]: [changedProp: K, newPropValue: Data[K]]
}[keyof Data]
As you correctly commented, the code above builds this union of tuples by mapping over the keys of Data
and indexing the result with keyof Data
. We also destructure the two tuple elements changedProp
and newPropValue
from the resulting tuple; a shortcut so we don't have to access the tuple to get the values later.
export function getChangedDataByProperty(
data: Data,
...[changedProp, newPropValue]: {
[K in keyof Data]: [changedProp: K, newPropValue: Data[K]]
}[keyof Data]
) {
if (changedProp === "b") {
return {
...data,
b: {
x: newPropValue.x,
y: newPropValue.y,
}
}
} else {
return {
...data,
x: newPropValue
}
}
}
Upvotes: 1