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 strings. 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 usekeydirectly in the template literal strings.The
Dateconstructor takes year/month/daynumbers as input, notstrings. So we need to convert the values tonumberviaNumber()(or via unary, or something).Since
DateHavingObject<K>is an intersection of two generic types, and the compiler doesn't like assigning to theKproperty. In order to prevent an error, we (mostly safely) upcastobjfromDateHavingObject<K>to just the{[P in K]?: Date}part, and then do the assignment theKproperty 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.
