skip to Main Content

Given the following types and components:

interface Widget { type: string; }
interface TextField extends Widget { type: 'TextField'; textValue: string; }
interface Checkbox extends Widget { type: 'Checkbox'; booleanValue: boolean; }

type Props<W extends Widget> = { widget: W; }
type WidgetComponent<W extends Widget> = (props: Props<W>) => React.ReactNode;

const TextfieldComponent: WidgetComponent<TextField> = ({ widget }) => { return <input type="text" /> };
const CheckboxComponent: WidgetComponent<Checkbox> = ({ widget }) => { return <input type="checkbox" />};

and the following predicates:

const isTextfield = (widget: Widget): widget is TextField => widget.type === 'TextField';
const isCheckbox = (widget: Widget): widget is Checkbox => widget.type === 'Checkbox';

How can I type properly the given function?

const factory = <W extends Widget>(widget: W): WidgetComponent<W> | null => {
    if (isTextfield(widget)) {
        return TextfieldComponent;
    } else if (isCheckbox(widget)) {
        return CheckboxComponent;
    }
    return null;
}

TypeScript does not like that and it tells me:

Type 'WidgetComponent<TextField>' is not assignable to type 'WidgetComponent<W>'.
  Type 'W' is not assignable to type 'TextField'.
    Property 'textValue' is missing in type 'Widget' but required in type 'TextField'.(2322)

I understand why TypeScript is giving me this error but I don’t know which mechanism should I use to give TypeScript the proper type constraints.

You can see it in action on the TypeScript playground

2

Answers


  1. const factory = <W extends Widget>(widget: W): WidgetComponent<W> | null => {
        if (isTextfield(widget) && widget.type === 'TextField') {
            return TextfieldComponent as WidgetComponent<W>;
        } else if (isCheckbox(widget) && widget.type === 'Checkbox') {
            return CheckboxComponent as WidgetComponent<W>;
        }
        return null;
    };
    
    Login or Signup to reply.
  2. You can achieve what you want using function overloads.

    
    // Overload signature (no body)
    function factory(widget: TextField): WidgetComponent<TextField>;
    
    // Overload signature (no body)
    function factory(widget: Checkbox): WidgetComponent<Checkbox>;
    
    // Implementation signature (with function body)
    function factory(widget: TextField | Checkbox) {
      if (isTextfield(widget)) {
        return TextfieldComponent;
      } else if (isCheckbox(widget)) {
        return CheckboxComponent;
      }
    }
    

    The solution is hard to scale if you have many types of Widget, as you need an overload for each of them, but it should be fine for your case.

    You can test with:

    const myTextField: TextField = { type: 'TextField', textValue: 'foo' };
    const myCheckbox: Checkbox = { type: 'Checkbox', booleanValue: true };
    
    const textFieldComp = factory(myTextField);
    const checkboxComp = factory(myCheckbox);
    

    Your IDE type hint should correctly identify textFieldComp as WidgetComponent<TextField> and checkboxComp as WidgetComponent<Checkbox>


    P.S: You should probably be using ReactElement | null as the return type for your WidgetComponent function component. Using ReactNode will give you trouble when rendering JSX. See, for instance: When to use JSX.Element vs ReactNode vs ReactElement?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search