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