You want parseRecordDates() to be generic both in the type T of the record parameter, and the type K corresponding to just those keys of T present in the fields parameter. And we should constrain T or K or both so that the property type of T at the keys K are known to hold string properties.
The return type will be { [P in keyof T]: P extends K ? Date : T[P] }. That's a mapped type where each key P from the keys of T will be a key of the return type, and where each property is a conditional type that checks if P is included in the fields element type K: if so, then the property will be of the Date type, otherwise it is unchanged (and stays T[P], the indexed access type corresponding to the property value of T and the index P).
Here's one way to write it:
const parseRecordDates = <
T extends Record<K, string>,
K extends keyof T
>(
record: T, fields: K[]
): { [P in keyof T]: P extends K ? Date : T[P] } => {
const result = { ...record } as any
for (const field of fields) {
const value = record[field]
result[field] = new Date(value)
}
return result;
}
Here I've constrained T so that it is known to have string properties at the K keys.
I used the any type to loosen the type checking on result and avoid compiler errors. In general, we can't count on the TypeScript compiler to verify that a function implementation properly returns a generic mapped conditional type. It will either be too lenient and allow bad implementations, or too strict and complain about correct implementations. I'm just sidestepping this by using any. There are other approaches, but no matter what, you want to be careful inside the function implementation.
That implementation should work as desired:
const json: SequenceJson =
{ sequenceId: 123, rowId: "abc", sequenceTimestamp: new Date().toString() };
const seq = parseRecordDates(json, ['sequenceTimestamp']);
/* const seq: {
rowId: string;
sequenceId: number;
sequenceTimestamp: Date;
} */
console.log(seq.sequenceTimestamp.getFullYear()) // 2023
function acceptSequence(x: Sequence) { }
acceptSequence(seq) // okay
It also produces an error when you call it incorrectly:
parseRecordDates(json, ['sequenceId']); // error
// ------------> ~~~~
// Argument of type 'SequenceJson' is not assignable
// to parameter of type 'Record<"sequenceId", string>'.
although the location of the error might be surprising, as it complains about json and not "sequenceId".
If you care about the location of that error, you can augment the call signature of parseRecordDates() to constrain K further than just keyof T, to just those keys of T whose property value types are assignable to string. We can call that KeysMatching<T, string>... but for now anyway we need to implement KeysMatching ourselves. One implementation of KeysMatching is
type KeysMatching<T, V> =
keyof { [K in keyof T as T[K] extends V ? K : never]: any }
(See In TypeScript, how to get the keys of an object type whose values are of a given type? for more information about various implementations). And then we change the constraint on K:
const parseRecordDates = <
T extends Record<K, string>,
K extends KeysMatching<T, string> // <-- change is here
>(
record: T, fields: K[]
): { [P in keyof T]: P extends K ? Date : T[P] } => {
const result = { ...record } as any
for (const field of fields) {
const value = record[field]
result[field] = new Date(value)
}
return result;
}
And now we see the error in a more expected place.
parseRecordDates(json, ['sequenceId']); // error
// -------------------> ~~~~~~~~~~~~
// Type '"sequenceId"' is not assignable to type '"sequenceTimestamp" | "rowId"'.
Playground link to code