Ben Baldwin
Ben Baldwin

Reputation: 497

Generic react component props

I'm building a form component in react using typescript that takes a 'fieldStructures' and an 'onSubmit' prop.

type FieldStructure {
    type: 'text';
    name: string;
    label?: string;
    helpText?: string;
    required?: boolean;
    multiline?: boolean;
} | {
    type: 'date';
    name: string;
    label?: string;
    helpText?: string;
    required?: boolean;
} | {
    type: 'option';
    name: string;
    label?: string;
    helpText?: string;
    required?: boolean;
    options: string[];
    multi?: boolean;
} | {
    type: 'file';
    name: string;
    label?: string;
    helpText?: string;
    required?: boolean;
    fileTypes?: string[];
}

type FieldValue <T extends FieldType = any> =
    T extends 'text' ? string
    : T extends 'date' ? Date | null
    : T extends 'option' ? string | string[] | null
    : T extends 'file' ? string | File | null
    : string | string[] | Date | File | null

interface FormModalProps {
    fieldStructures: FieldStructure[];
    handleSubmit: (values: { [key: string]: FieldValue }) => void | Promise<void>;
}

export const FormModal: React.FC<FormModalProps> = ({
    fieldStructures,
    handleSubmit
}) => { ... }

This works great for just making the component but not so much for using it. I want the type of the values parameter to rely on what is passed to the fieldStructures parameter like this...

interface FormValues <T extends FieldStructure[]> {
    //keys are the names of each field structure
    //values are FieldValue<field structure type>
}

interface FormModalProps <T extends FieldStructure[]> {
    fieldStructures: T;
    handleSubmit: (values: FormValues<T>) => void | Promise<void>;
}

This example feels like an over-simplification, but does anyone know of a way to achieve the same result?

the full code is available on my github at: https://github.com/baldwin-design-co/bdc-components/blob/master/src/form%20modal/form-modal.tsx

Upvotes: 2

Views: 229

Answers (1)

Guilhermevrs
Guilhermevrs

Reputation: 2344

You should be specifying a type for each structure, that extends a common type with a designated property (in your case, type). Then for each form, you need to receive a config that extends a common config as well. The FormValue than will be a simple keyof T.

Check this example below:

// Field types

export interface FieldStructure {
    type: 'text' | 'date';
}

export interface TextField extends FieldStructure {
    type: 'text';
    label: string;
}

export interface DateField extends FieldStructure {
    type: 'date';
    label: string;
}

// Calculate the value based on field type

export type FieldValue<T extends FieldStructure> = 
    T extends TextField ? string
    : T extends DateField ? Date | null
    : string | Date | null ;
    
// Types the form itself

type FormConfig = {[key: string]: FieldStructure}

// Gets the form value based on the type of each field
type FormValue<T extends FormConfig> = {
    [k in keyof T]: FieldValue<T[k]>
}


// Example of usage:

interface MyFormConfig extends FormConfig {
   a: TextField;
   b: DateField
}

Upvotes: 1

Related Questions