[React Typescript] Discriminated unions in components props

import React, { useState } from "react";

type Base = { id: string } | string;

type GenericSelectProps<TValue> = {
  formatLabel: (value: TValue) => string;
  values: Readonly<TValue[]>;
};

interface SingleSelectProps<TValue> extends GenericSelectProps<TValue> {
  isMulti: false;
  onChange: (value: TValue) => void;
}

interface MultiSelectProps<TValue> extends GenericSelectProps<TValue> {
  isMulti: true;
  onChange: (value: TValue[]) => void;
}

type T = { id: string };

const isStringValue = <TValue extends T>(
  value: TValue | string
): value is string => {
  return typeof value === "string";
};

const getStringFromValue = <TValue extends Base>(value: TValue) => {
  if (isStringValue(value)) return value;

  return value.id;
};

export const GenericSelect = <TValue extends Base>(
  props: SingleSelectProps<TValue> | MultiSelectProps<TValue>
) => {
  const { values, formatLabel } = props;
  const [selectedValues, setSelectedValues] = useState<TValue[]>([]);

  const onSelectChange = (e) => {
    const val = values.find(
      (value) => getStringFromValue(value) === e.target.value
    );
    if (!val) return;

    if (props.isMulti) {
      const newValues = [...selectedValues, val];
      props.onChange(newValues);
      setSelectedValues(newValues);
    } else {
      if (val) props.onChange(val);
    }
  };

  const onDeleteValue = (index: number) => {
    return () => {
      const newValues = selectedValues.filter((v, i) => i !== index);
      if (props.isMulti) {
        props.onChange(newValues);
        setSelectedValues(newValues);
      }
    };
  };
  const visibleValues = !props.isMulti
    ? values
    : values.filter((v) => !selectedValues?.includes(v));

  return (
    <>
      {props.isMulti ? (
        <>
          {selectedValues.map((v, index) => (
            <span onClick={onDeleteValue(index)}>{formatLabel(v)} [x]</span>
          ))}
          <br />
        </>
      ) : (
        ""
      )}
      <select onChange={onSelectChange} multiple={props.isMulti}>
        {visibleValues.map((value) => (
          <option
            key={getStringFromValue(value)}
            value={getStringFromValue(value)}
          >
            {formatLabel(value)}
          </option>
        ))}
      </select>
    </>
  );
};

 

Usage:

const select = (
  <GenericSelect<Book>
    // I can't log "value.title" here, typescript will fail
    // property "title" doesn't exist on type "Book[]""
    // even if I know for sure that this is a single select
    // and the type will always be just "Book"
    onChange={(value) => console.info(value.title)}
    isMulti={false}
    ...
  />
);

const multiSelect = (
  <GenericSelect<Book>
    // I can't iterate on the value here, typescript will fail
    // property "map" doesn't exist on type "Book"
    // even if I know for sure that this is a multi select
    // and the type will always be "Book[]"
    onChange={(value) => value.map(v => console.info(v))}
    isMulti={true}
    ...
  />
);

 

Refer: https://www.developerway.com/posts/advanced-typescript-for-react-developers-discriminated-unions

 

posted @ 2022-11-21 15:48  Zhentiw  阅读(15)  评论(0编辑  收藏  举报