2

I'm struggling to get a complex type functionality out of this updateArray generic function I am building:

// Updates an object array at the specified update key with the update value,
// if the specified test key matches the test value.
// Optionally pass testFailValue to set a default value if the test fails.
// Note that by passing a testFailValue ALL elements in the array will be updated at the specified update property. 
// If it is omitted only elements passing the test will be updated.
export const updateArray = <T, U, V>(options: {
  array: Array<T>
  testKey: keyof T
  testValue: U
  updateKey: keyof T
  updateValue: V
  testFailValue?: V
}): Array<T> => {
  const {
    array,
    testKey,
    testValue,
    updateKey,
    updateValue,
    testFailValue,
  } = options
  return array.map(item => {
    if (item[testKey] === testValue) {
      item[updateKey] = updateValue
    } else if (testFailValue !== undefined) {
      item[updateKey] = testFailValue
    }
    return item
  })
}

TypeScript will complain here in both the if statement and the two assignment statements, but in a call signature, it won't complain and is the strict type checking I am looking for exactly, ex:

interface IMyInterface {
    propertyA: string
    prepertyB: boolean
}

updateArray<IMyInterface, IMyInterface['propertyA'], IMyInterface['propertyB']>({
    array: state.editors[editor].editorSettings,
    testKey: "propertyA",
    testValue: 'someValue',
    updateKey: "propertyB",
    updateValue: true,
    testFailValue: false
})

If I omit types U and V, and replace them with T[keyof T] Typescript won't complain:

export const updateArray = <T>(options: {
  array: Array<T>
  testKey: keyof T
  testValue: T[keyof T]
  updateKey: keyof T
  updateValue: T[keyof T]
  testFailValue?: T[keyof T]
}): Array<T> => {
  const {
    array,
    testKey,
    testValue,
    updateKey,
    updateValue,
    testFailValue,
  } = options
  return array.map(item => {
    if (item[testKey] === testValue) {
      item[updateKey] = updateValue
    } else if (testFailValue !== undefined) {
      item[updateKey] = testFailValue
    }
    return item
  })
}

but this isn't entirely correct either. T[keyof T] is too flexible: I could be assigning the 'wrong' type to a given property (for example, in the example given, a boolean value to a property that should only be holding strings, or vice-versa). Obviously, this reassigning type behavior is fine in JavaScript (which is one reason why TypeScript won't complain), but undesired for this function I am crafting. Really what I need is some sort of typeof T[specific key], to ensure that testValue, updateValue, and testFailValue correspond with the right types, but the specific key can change depending on the actual type of T.

Can something like this be done?

fullStackChris
  • 1,300
  • 1
  • 12
  • 24

1 Answers1

3

You can add a constraint on U so that it is a subset of the keys of T using extends. V could represent the updateKey type and also have the same constraint.

Simplifying your problem to a funtion updateObject instead of updateArray it would become:

function updateObject<
    T,
    U extends keyof T,
    V extends keyof T,
>(
    obj: T,
    testKey: U,
    testValue: T[U],
    updateKey: V,
    updateValue: T[V],
    testFailValue?: T[V]
) {
    if (obj[testKey] === testValue) {
        obj[updateKey] = updateValue;
    } else if (testFailValue !== undefined) {
        obj[updateKey] = testFailValue;
    }
}

updateObject({aString: 'hello', aNumber: 42}, 'aString', 'hello', 'aNumber', 23);
Frank Bessou
  • 632
  • 5
  • 8