I'm trying to work out how to use generics to convert a date which is split into multiple parts into a Date object.
So far, I have this:
export const convertDate = <T, K extends keyof T>(obj: T, key: K) => {
const k = String(key)
const [month, day, year] = [obj[`${k}-month`], obj[`${k}-day`], obj[`${k}-year`]]
if (month && day && year) {
obj[key] = new Date(year, month, day)
}
return obj
}
Which I'd like to use like so:
interface MyObjectWithADate {
date?: Date
['date-year']: string
['date-month']: string
['date-day']: string
}
const obj: MyObjectWithADate = {
'date-year': '2022',
'date-month': '12',
'date-day': '11',
}
convertDate(obj, 'date')
# obj.date = new Date(2022, 12, 11)
However, the compiler gives me the error Type 'Date' is not assignable to type 'T[K]'.
How do I ensure my object can recieve a type of Date
?
Playground link is below:
CodePudding user response:
The main problem with your version of convertDate
is that it is generic in the type T
of obj
, but T
isn't known to have a Date
-valued property at key K
(we know K extends keyof T
, so T
has some property at K
, but it could be of any type whatsoever). Furthermore, T
isn't known to have keys at `${K}-year`
, `${K}-month`
, or `${K}-day`
, so you can't safely index into obj
with those keys.
If you know K
is the type of the key
parameter, then we can express the type of obj
in terms of it without needing to have another generic type parameter. It looks something like this:
type DateHavingObj<K extends string | number> =
{ [P in `${K}-${"year" | "month" | "day"}`]: string } &
{ [P in K]?: Date };
That's an intersection of two mapped types. First we have an object type whose keys are template literal types you get when you concatenate K
to "-year"
, "-month"
, or "-day"
, and whose property values are string
s. And then we have an object type with an optional property whose key is K
and whose value is Date
.
Now the call signature is like
const convertDate = <K extends string | number>(
obj: DateHavingObj<K>, key: K
) => { }
And we can see that it works when you call it on your MyObjectWithADate
-typed obj
if key
is "date"
:
convertDate(obj, "date"); // okay
but fails if you call it with some other key
:
convertDate(obj, "fake"); // error!
// -------> ~~~
/* Type 'MyObjectWithADate' is missing properties
"fake-year", "fake-month", "fake-day" */
Anyway, we need to tweak the implementation of convertDate()
a bit to make it compile with no errors:
const convertDate = <K extends string | number>(
obj: DateHavingObj<K>, key: K
) => {
const [month, day, year] = [
Number(obj[`${key}-month`]),
Number(obj[`${key}-day`]),
Number(obj[`${key}-year`])
];
const o: { [P in K]?: Date } = obj;
if (month && day && year) {
o[key] = new Date(year, month, day)
}
return obj;
}
The changes I made:
We don't need to write
String(key)
if we're just going to use the result inside a template literal string. And the compiler doesn't understand thatString(key)
results in a value of type`${K}`
, but it does understand that the`${key}`
results in a value of that type. So we might as well usekey
directly in the template literal strings.The
Date
constructor takes year/month/daynumber
s as input, notstring
s. So we need to convert the values tonumber
viaNumber()
(or via unarySince
DateHavingObject<K>
is an intersection of two generic types, and the compiler doesn't like assigning to theK
property. In order to prevent an error, we (mostly safely) upcastobj
fromDateHavingObject<K>
to just the{[P in K]?: Date}
part, and then do the assignment theK
property of that.
And let's make sure it still works:
console.log(obj.date?.toUTCString()) // "Wed, 11 Jan 2023 06:00:00 GMT"
Looks good.