'Typescript mapped and conditional type not properly resolved

I am working on a project where I am trying to create types from an object. The input object is considered as the source, and is used to generate a definition (this mechanism allow me to set default values, as seen in makeAttributeDefinition) and then this definition is used to generate an entity.

To type the entity, the source defines attributes using the as const constraint, and the entity type is based on a generic that use mapped and conditional types.

However, the conditional typing does not seems to work as expected, as the types comparisons fail, and properties types on my entity ends up being resolved to the fallback type that is never

Here is the code:

// ######################################## ATTRIBUTE DEFINITION ########################################
type AttributeDefinitionSource = {
    name?: string
    dataType?: "string" | "number"
}

type AttributeDefinition<S extends AttributeDefinitionSource> = {
    name: S['name'] extends AttributeDefinitionSource['name'] ?  S['name'] : 'defaultName'
    dataType: S['dataType'] extends AttributeDefinitionSource['dataType'] ?  S['dataType'] : 'string'
}

function makeAttributeDefinition<S extends AttributeDefinitionSource>(source:S) {
    return {
        name: source.hasOwnProperty('name') ? source.name : 'defaultName',
        dataType: source.hasOwnProperty('dataType') ? source.dataType : 'string',
    } as AttributeDefinition<S>
}
// ######################################## ENTITY DEFINITION ########################################
type EntityDefinitionSource = {
    attributes?: Readonly<AttributeDefinitionSource[]>
}

type EntityDefinition<S extends EntityDefinitionSource> = {
    attributes: { [I in keyof S['attributes']]: AttributeDefinition<S['attributes'][I]>}
}

function makeEntityDefinition<S extends EntityDefinitionSource>(source:S){
    return {
        attributes: (source.attributes || []).map((attr) => makeAttributeDefinition(attr))
    } as unknown as EntityDefinition<S>
}

// ######################################## ENTITY ########################################
type Entity<ET extends EntityDefinition<EntityDefinitionSource>> = {
    [K in ET['attributes'][number] as K['name']]:
    K['dataType'] extends 'string' ?  string :
    K['dataType'] extends 'number' ?  number :
    never
}


// ######################################## USAGE ########################################
const entityDefinitionSource = {
    attributes: [
        {name: 'foo', dataType: 'string'},
        {name: 'baz', dataType: 'number'},
    ] as const
}

const entityDefinition = makeEntityDefinition(entityDefinitionSource)
                                                                        // types (not values) from IntelliSense
const firstAttributeName = entityDefinition.attributes[0].name          // firstAttributeName: "foo"
const firstAttributeDataType = entityDefinition.attributes[0].dataType  // firstAttributeDataType: "string"
const secondAttributeName = entityDefinition.attributes[1].name         // secondAttributeName: "baz"
const secondAttributeDataType = entityDefinition.attributes[1].dataType // secondAttributeDataType: "number"

const entity: Entity<typeof entityDefinition> = {
    foo: 'hello world', // TS2322: Type 'string' is not assignable to type 'never'.
    baz: 42
}

The entityDefinition seems to be properly typed as my IDE (thanks to IntelliSense) confirms me that attributes name and dataType have expected types (as mentioned in my comments).

This can be confirmed by simplifying the Entity type as follow (the entity will only accept keys from the attributes name property on the source - which confirms me that the entityType type is properly interpreted, and that the issue seems to come from the conditional typing in the original Entity type):

type EntityT<ET extends EntityDefinition<EntityDefinitionSource>> = {
    [K in ET['attributes'][number] as K['name']]:any
}

I am not sure to identify what I am doing wrong, and even more that this code is the evolution of this one that was working as expected. Am I doing something wrong?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source