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?
CodePudding user response:
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.