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?
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:
- Pass the fields directly to the function inline:
createForm({
numericField: {
type: 'numeric'
},
textField: {
type: 'text'
}
}, data)
- Use
as const
to prevent type widening of the fields variable:
const fields2 = {
numericField: {
type: 'numeric'
},
textField: {
type: 'text'
}
} as const;
createForm(fields, data)
- 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 })