Znar
Znar

Reputation: 203

How to create a relation between parameters of a function?

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}
}

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

Answers (1)

Tobias S.
Tobias S.

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
    }
  }
}

Playground

Upvotes: 1

Related Questions