Not sure if the title is explicit (it was not easyto describe this issue in one sentence) but here is some code to illustrate the issue (more details about the issue after the code block):
type AttributeDefinition = {
name:string
type: 'string' | 'number',
}
type EntityDefinition = {
attributes: Readonly<AttributeDefinition[]>
}
export type Entity<ET extends EntityDefinition> = {
[K in ET['attributes'][number] as K['name']]:
K['type'] extends 'string' ? string :
K['type'] extends 'number' ? number :
never
}
const def1 = {
attributes: [
{name:'foo', type: 'string'},
{name:'baz', type: 'number'},
] as const
}
const def2: EntityDefinition = {
attributes: [
{name:'foo', type: 'string'},
{name:'baz', type: 'number'},
] as const
}
const entity1: Entity<typeof def1> = {
foo: 'bar',
baz: 42
}
const entity2: Entity<typeof def2> = {
foo: 'bar',
baz: 42
}
So my issue is that typescript throw an error on entity2
when I want to assign 'bar'
to foo
(and 42
to baz
). The issue is TS2322: Type 'string' is not assignable to type 'never'
.
However, this issue does not occurs with entity1
.
The origin of this issue seems to be related to the declaration of def2
for which I annotate the EntityDefinition
type. I am doing this so I can have typing hint from my IDE helping me to properly declare the def2
object.
But the def1
object, even if not type annotated, is later properly handled when creating the entity1
, whereas def2
(that I would expect to be typed the right way) fails when it is used to create the entity2
.
I am not really sure to understand why it does fail in such a way, and how can I annotate a object as I do for def2
and also use such an object later as I do for entity1
.
Thanks!
CodePudding user response:
The reason Entity<typeof def2>
fails is that the annotation widens the type of def2
, which causes the specifics to get lost. The type of Entity<typeof def2>
is equal to Entity<EntityDefinition>
, which evaluates to {[x: string]: never}
.
You could probably create a generic type annotation that preserves the information, but then you'd have to specify the generic parameters in the constant type, which would lead to duplicated code.
On approach to deal with this is to create a constructor function that just returns the argument but has a constraint on it:
const mkEntityDefinition = <T extends EntityDefinition>(def: T) => def
You can use it like this:
const def3 = mkEntityDefinition({
attributes: [
{name:'foo', type: 'string'},
{name:'baz', type: 'number'},
] as const
})
export const entity3: Entity<typeof def3> = {
foo: 'bar',
baz: 42
}
and it will prevent mistakes:
const defBad = mkEntityDefinition({
attributes: [
{name:'foo', type: 'strrring'},
{name:'baz', type: 'number'},
] as const
})
// Error: Type '"strrring"' is not assignable to type '"string" | "number"'.
// Did you mean '"string"'?
CodePudding user response:
Until the time that satifies
makes it into TS, you can use a constrained identity function to provide IntelliSense assistance while physically typing your definition in your editor, while still allowing the compiler to infer its (more specific) literal properties:
function createEntityDefinition <T extends EntityDefinition>(def: T): T {
return def;
}
const def2 = createEntityDefinition({
attributes: [
{name:'foo', type: 'string'},
{name:'baz', type: 'number'},
] as const,
});
const entity2: Entity<typeof def2> = {
foo: 'bar',
baz: 42,
}; // ok