Search code examples
typescriptgenericsenumstypescript-generics

Infer enum from object property in typescript with generics


I have a scenario where I want to validate commands from their schema id and return typed validation results. For this, I need to be able to infer the enum value of the schema id.

I know I can use generics for this, like TSchemaId extends SchemaId, and it works. However, when I try to infer it from inside an object, it doesn't behave as I thought it would.

Here's what I have

enum SchemaId {
  SCHEMA_1 = 'SCHEMA_1',
  SCHEMA_2 = 'SCHEMA_2',
}

function inferSchemaId<TSchemaId extends SchemaId> (schemaId: TSchemaId): TSchemaId {
  return schemaId
}

function inferObjectSchemaId<
    TSchemaId extends SchemaId,
    TObjectWithSchemaId extends { schemaId: TSchemaId },
> (objectWithSchemaId: TObjectWithSchemaId): TSchemaId {
  return objectWithSchemaId.schemaId
}

const si1 = inferSchemaId(SchemaId.SCHEMA_1)                     // SchemaId.SCHEMA_1
const si2 = inferObjectSchemaId({ schemaId: SchemaId.SCHEMA_1 }) // SchemaId

As you can see above, si1 is correctly narrowed to SchemaId.SCHEMA_1, but si2 cannot be narrowed down.

Why is it the case? What am I missing?


Solution

  • The problem you're having is that the generic constraint ObjectWithSchemaId extends { schemaId: TSchemaId } is not an inference site for the TSchemaId type parameter. Such functionality was suggested at microsoft/TypeScript#7234 but it was never implemented, since you can usually get the desired behavior in other ways. That means there's nowhere at all for TypeScript to infer TSchemaId from, so it falls back to its constraint of SchemaId, and everything follows from that.


    You'll need to refactor if you want to change how it works. You could either just accept TObjectWithSchemaId and compute TSchemaId from it as an indexed access type

    function inferObjectSchemaId<
      TObjectWithSchemaId extends { schemaId: SchemaId },
    >(objectWithSchemaId: TObjectWithSchemaId): TObjectWithSchemaId["schemaId"] {
      return objectWithSchemaId.schemaId
    }
    

    or just accept TSchemaId and compute an appropriate type for objectWithSchemaId form it:

    function inferObjectSchemaId<
      TSchemaId extends SchemaId,
    >(objectWithSchemaId: { schemaId: TSchemaId, [k: string]: any }): TSchemaId {
      return objectWithSchemaId.schemaId
    }
    

    or take some other approach that doesn't involve trying to infer from constraints.

    Playground link to code