1

This question is similar to this one, but I think it lacks a lot of the main caveats and concerns raising issues there because I'm not making any assumptions that the properties of a given interface are a closed or ordered set.

Suppose I have an interface and couple of functions like this:

interface Person {
    givenNames: string,
    familyNames: string,
    charactersToDisplayNames: number,
    age: number,
    weight: number,
    fastest5KTime: number,
    //... could be plenty of others
}
function writeFields<
    T extends object,
    F extends (keyof T)[]
>(
    dest : T, 
    fieldsToWrite : F,
    valuesToWrite : WhatTypeGoesHere<T, F>, //Can be defined above
) {/*...*/}
function setNames(dest: Person, givenNames: string, familyNames: string) {
    writeFields(
        dest, 
        ['givenNames', 'familyNames', 'charactersToDisplayNames'],
        //In this case, the 3rd param should be
        //a tuple of type [string, string, number]
        [givenNames, familyNames, givenNames.length + 1 + familyNames.length]
    )
}
//other functions set other fields, or do so with parameters, etc...
//the desire is to avoid any non-type refactoring esp. to writeFields(). 

How should the type noted above as WhatTypeGoesHere be defined so that the third parameter has proper type-checking?

WBT
  • 2,249
  • 3
  • 28
  • 40
  • You should be [constraining](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints) `F` to `(keyof T)[]` via `F extends (keyof T)[]` and not making it a [default](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) via `F = (keyof T)[]`. Please [edit] that since I don't think you're trying to ask about this, but it won't work the way you have. – jcalz Aug 10 '22 at 22:04

1 Answers1

1

You can give the valuesToWrite property a mapped tuple type where you take every element type in the fieldsToWrite type (I'm changing this from F to K) and index into T with it.

Like this:

function writeFields<
  T extends object,
  K extends readonly (keyof T)[]
>(
  dest: T,
  fieldsToWrite: readonly [...K],
  valuesToWrite: { [I in keyof K]: T[K[I]] },
) {/*...*/ }

I'm giving fieldsToWrite a variadic tuple type of the form [...K] instead of just K to give the compiler a hint that it should infer a tuple type and not an unordered array type from the fieldsToWrite argument.

Also, it's not super important whether or not the tuple types are mutable or readonly tuple types but readonly ones are less restrictive (e.g., string[] is assignable to readonly string[] but not vice versa) so I used readonly.

Let's test it out:

writeFields(
  dest,
  ['givenNames', 'familyNames', 'charactersToDisplayNames'],
  [givenNames, familyNames, givenNames.length + 1 + familyNames.length]
) // okay
// T is Person, K is ["givenNames", "familyNames", "charactersToDisplayNames"]

writeFields(
  dest,
  ['age', 'familyNames'],
  ['oops', 'okay'] // error!
  //~~~~~ <-- Type 'string' is not assignable to type 'number'.(2322)
);

Looks good. The compiler knows which values belong at which index when you call writeFields() and will complain if you pass in something bad.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks much, this is quite helpful! However, it seems to fail on a [modest extension](https://tsplay.dev/NrnJ3N) (still simplified in terms of not having proper limits/error handling/etc.) in which type T is pulled from a map. Am I missing something fairly obvious, or is this solution brittle to extensions like that? – WBT Aug 11 '22 at 21:38