mTv
mTv

Reputation: 1356

How can I make TypeScript know which proptypes I am using?

How can I make TypeScript know which of the prop types the component is receiving? E.g if multiple is true, I want TypeScript to expect that selectValue is an array of string or if its not present then TypeScript should expect selectValue to be a string.

I really dislike having to cast my props to the appropriate type for TypeScript to be happy.

Here is the current code for my custom <Select> component:

import React, { ReactNode } from 'react';
import MultipleSelect from './MultipleSelect';
import SingleSelect from './SingleSelect';

interface MultipleSelectProps {
  selectValue: string[];
  multiple: true;
  onSelectChange: (values: string[]) => void;
  renderValue?: (value: string[]) => React.ReactNode;
}

interface SingleSelectProps {
  selectValue: string;
  onSelectChange: (values: string) => void;
  multiple?: never;
  renderValue?: (value: string) => React.ReactNode;
}

type SelectProps = SingleSelectProps | MultipleSelectProps;

const Select: React.FC<SelectProps> = ({
  selectValue,
  multiple,
  onSelectChange,
  renderValue,
  children,
}) => {
  if (multiple) {
    const multipleSelectValue = selectValue as string[];
    const multipleSelectChange = onSelectChange as (value: string[]) => void;
    const multipleRenderValue = renderValue as (value: string[]) => ReactNode;

    return (
      <MultipleSelect
        selectValue={multipleSelectValue}
        onSelectChange={multipleSelectChange}
        renderValue={multipleRenderValue}
      >
        {children}
      </MultipleSelect>
    );
  }

  const singleSelectValue = selectValue as string;
  const singleSelectChange = onSelectChange as (value: string) => void;
  const singleRenderValue = renderValue as (value: string) => ReactNode;

  return (
    <SingleSelect
      selectValue={singleSelectValue}
      onSelectChange={singleSelectChange}
      renderValue={singleRenderValue}
    >
      {children}
    </SingleSelect>
  );
};
export default Select;

I want to write it something like this instead:

import React, { ReactNode } from 'react';
import MultipleSelect from './MultipleSelect';
import SingleSelect from './SingleSelect';

interface MultipleSelectProps {
  selectValue: string[];
  multiple: true;
  onSelectChange: (values: string[]) => void;
  renderValue?: (value: string[]) => React.ReactNode;
}

interface SingleSelectProps {
  selectValue: string;
  onSelectChange: (values: string) => void;
  multiple?: never;
  renderValue?: (value: string) => React.ReactNode;
}

type SelectProps = SingleSelectProps | MultipleSelectProps;

const Select: React.FC<SelectProps> = ({
  selectValue,
  multiple,
  onSelectChange,
  renderValue,
  children,
}) => {
  return (
{
  multiple ?
    <MultipleSelect
      selectValue={selectValue}
      onSelectChange={onSelectChange}
      renderValue={renderValue}
    >
      {children}
    </MultipleSelect>
    :
    <SingleSelect
      selectValue={selectValue}
      onSelectChange={onSelectChange}
      renderValue={renderValue}
    >
      {children}
    </SingleSelect>
  );
};
export default Select;

and I want to be able to use my component similarly to how the native html <select> is used. Like so:

<Select selectValue={selectValue} onSelectChange={onSelectChange} multiple>
  {options}
</Select>

Upvotes: 1

Views: 209

Answers (2)

giggo1604
giggo1604

Reputation: 511

You can use into check if multiple is set like so:

type MultipleSelectProps = {
  multiple: true;
  value: string[];
  onChange: (newValue: string[]) => void;
};

type SingleSelectProps = {
  value: string;
  onChange: (newValue: string) => void;
};

type SelectProps = MultipleSelectProps | SingleSelectProps;

function SingleSelect(props: SingleSelectProps) {
  return null;
}

function MultiSelect(props: MultipleSelectProps) {
  return null;
}

function Select(props: SelectProps) {
  return "multiple" in props ? (
    <MultiSelect {...props} />
  ) : (
    <SingleSelect {...props} />
  );
}

Upvotes: 1

Boudewijn Danser
Boudewijn Danser

Reputation: 111

This is how I would do it. By defining the kind TypeScript is aware of the type of selectValue and other that might change.

import React, { ReactNode } from 'react';
import MultipleSelect from './MultipleSelect';
import SingleSelect from './SingleSelect';


type SelectProps = {
    kind: 'multiple',
    selectValue: string[];
    onSelectChange: (values: string[]) => void;
    renderValue?: (value: string[]) => React.ReactNode;
} | {
    kind: 'single',
    selectValue: string;
    onSelectChange: (values: string) => void;
    renderValue?: (value: string) => React.ReactNode;

}

const Selector = (props: SelectProps, children): JSX.Element => {
  return (
    <>
        {
        props.kind === 'multiple' 
        ?
            <MultipleSelect
            selectValue={props.selectValue}
            onSelectChange={props.onSelectChange}
            renderValue={props.renderValue}
            >
            {children}
            </MultipleSelect>
        :
            <SingleSelect
            selectValue={props.selectValue}
            onSelectChange={props.onSelectChange}
            renderValue={props.renderValue}
            >
            {children}
            </SingleSelect>
        
        }
    </>
  )
}

export default Selector

Upvotes: 1

Related Questions