Home > Blockchain >  Generic interface, which can map object properties to another interface
Generic interface, which can map object properties to another interface

Time:10-19

I am trying to make types for my FormBuilder component, but got some troubles with property type extraction.

here is simplified code:

interface NumericField {
    type: 'numeric'
}

interface TextField {
    type: 'text'
}

type Field = NumericField | TextField

type Fields = Record<string, Field>

export type PropertyTypeExtractor<T extends Fields> = {
    [Property in keyof T]
    : T[Property] extends NumericField ? number
    : T[Property] extends TextField ? string 
    : never;
};

const fields = {
    numericField: {
        type: 'numeric'
    },
    textField: {
        type: 'text'
    }
}

const data = {
    numericField: 42,
    textField: 'lorem ipsum'
}

function createForm<T extends Fields>(fields: T, data: PropertyTypeExtractor<T>) { }

// fields: Type 'string' is not assignable to type '"text"'.
createForm(fields, data)

As i can see, the main problem goes from type Fields = Record<string, Field>, typescript doesn't know which exactly type comes to Field. Even if type === 'text' means this can be the only TextField.

Typescript smart enough for this:

const anotherFields: Fields = {
    numericField: {
        type: 'numeric'
    },
    textField: {
        type: 'text'
    }
}

const field = anotherFields.numericField // field: Field
if (field.type === 'numeric') {
    console.log(field) // field: NumericField
}

so is here some workaround for my case?

Playground

CodePudding user response:

The problem is that typescript is evaluating and widening your type to type Fields when passed as an argument to the function. You can solve this in a few ways:

  1. Pass the fields directly to the function inline:
createForm({
    numericField: {
        type: 'numeric'
    },
    textField: {
        type: 'text'
    }
}, data)

  1. Use as const to prevent type widening of the fields variable:
const fields2 = {
    numericField: {
        type: 'numeric'
    },
    textField: {
        type: 'text'
    }
} as const;

createForm(fields, data)

  1. Join into one parameters type, so the inference applies to the whole object. I'm not 100% on the fundamentals on why this works - but I believe it's something to do with the inference for the type happening at the same time, rather than trying to find a best fit when comparing two separate types:
type FormParameters<T extends Fields> = { fields: T, data: PropertyTypeExtractor<T> };

function createForm<T extends FormParameters<any>>(parameters: T){ }

createForm({ fields, data })
  • Related