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?
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.