Home > OS >  Check if an object has a property of a certain type
Check if an object has a property of a certain type

Time:07-23

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:

https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM7ycgbsWARAQxmDgF44AeAFQBo4BpOUY5AE1TgGtgBPCAMzhUAfAAoIAIwBWALiF1uPOfQCUZYXADeAWABQcRCnRcycAMowoAS2QBzUYpV6DmYwG0AtihgALOqwIeOh5gAigAXVM3SSk3AAMAEk1OAF8AWi9kXzjwuhj4pNS0gJ4cvOkC5PSQsJzwvWc4K0FRTN84ADIOuBLO7pqoNR19Azh8xUjyZGAAdzhCYlEBuja-HsCnEZSGkahgGABXKGQx6T1t3R29G2IofgIEEgBZHgB5aWAEGAB1K18AQQWJGGBgCxAA-HIgY03AByMHANIDWHhOToax2GHwoiI1YotGWGy2LEI4qBfFwdFE856VzwGJyF7vKSfH5-HyAnGmEFwbHEJGhKCwuSwgBMAAZRaLYTRGnzcd4fMLeQBGaWykbysk8ZWwlUqmU0y66TA4fA48TSOjy2EqIA

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 that String(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 use key directly in the template literal strings.

  • The Date constructor takes year/month/day numbers as input, not strings. So we need to convert the values to number via Number() (or via unary , or something).

  • Since DateHavingObject<K> is an intersection of two generic types, and the compiler doesn't like assigning to the K property. In order to prevent an error, we (mostly safely) upcast obj from DateHavingObject<K> to just the {[P in K]?: Date} part, and then do the assignment the K 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.

Playground link to code

  • Related