Infer value of passed property name

Viewed 37

I'm having a weird problem with generic inferences, which is easier to explain in code then words (so apologies for the vague title).

I have a type for conveying the status of a data request:

enum Status {INIT, PENDING, LOADED, ERROR}

type StatusObject = {
  status: Status,
  initiated: boolean,
  error: null | string
}

And then various data objects that include a property of this type, e.g.:

const obj1 = {
  requestedData: "some data",
  goodStatus: {
    status: Status.LOADED,
    initiated: true,
    error: null
  }
}

I have a function that will get passed an object like obj1 and the key name of the property of type StatusObject for further operations. Let's say:

function checkStatus<T extends string, U extends Record<T, StatusObject>>(
  statusObjectKey: T,
   theObject: U
) 

In general, this type of signature works. For example, the following allows the first call but not the second as you'd hope:

const obj1 = {
  requestedData: "some data",
  goodStatus: {
    status: Status.LOADED,
    initiated: true,
    error: null
  }
}

const obj2 = {
  data: "some other data",
  badStatus: true
}

checkStatus("goodStatus", obj1). //no error
checkStatus("badStatus", obj2) //error, because obj2 doesn't have a badStatus property of type StatusObject

But inside the function itself things don't work as I'd expect. Namely--

function checkStatus<T extends string, U extends Record<T, StatusObject>>(
  statusObjectKey: T,
   theObject: U
) {
  let testStatus: StatusObject
  testStatus = theObject[statusObjectKey] // no error here
  theObject[statusObjectKey] = testStatus // but an error here.  WHY?
}

On the second assignment, I don't understand why TS can't tell that theObject[statusObjectKey] is of type ObjectStatus (but does seem to be able to tell it in the first assignment).

Can anyone help me understand this? Is there a better way to type this kind of function?

Here's the code in a playground.

1 Answers

The error is:

Type 'StatusObject' is not assignable to type 'U[T]'

That declaration of let testStatus: StatusObject actually says that if a subtype of StatusObject is assigned to this variable, just treat it like a StatusObject instead of the subtype.


The problem is that U[T] in this function is a more specific type than StatusObject.

When a generic extends a constraint (as in U extends Record<T, StatusObject>) it's not saying that it needs to be exactly that interface, but whatever is passed in just needs to conform to that interface.

That means that this would be a valid object to pass to that function:

const obj1 = {
  requestedData: "some data",
  goodStatus: {
    status: Status.LOADED,
    initiated: true,
    error: null,
    whatever: 'bar' // added extra property
  }
}

However, when your function does this:

let testStatus: StatusObject
testStatus = theObject[statusObjectKey]

You are actually casting that to a more general type. theObject[statusObjectKey] is no longer a type that just extends StatusObject with that extra whatever key, and now is instead exactly a StatusObject, which lacks that extra property. The type passed in might require that extra property.

The type has now been widened (made more general, less specific). This means that it cannot necessarily be assign back to where it came from.

U[T] on the other hand, is a type that extends StatusObject, and typescript knows whatever else that type is and can check to make sure you're handling it right.


So the solution is type your temporary variable as U[T]:

let testStatus: U[T]

Or just let typescript infer it:

const testStatus = theObject[statusObjectKey]

Playground

Related