Можно ли создать интерфейсы, в которых несколько свойств не являются обязательными, но должно присутствовать хотя бы одно? - PullRequest
0 голосов
/ 27 сентября 2019

При работе с типами объединения обычно проще всего создать ADT с теговым объединением.Однако иногда это невозможно.

Типичным примером является компонент <Route/> React Router.Здесь есть три дополнительных параметра: component, render и children, но должно быть 1 и ровно 1 из этих 3 пройденных.

Для простоты я сократил типы здесь, но опубликованные в настоящее время типы выглядят примерно так:

interface Route {
  render?: string
  component?: string
  children?: string
  someOtherProps?: any
}

Они должны действительно быть похожими на это:

interface BetterRouteRender extends Route {
  render: string
  component?: undefined
  children?: undefined
}

interface BetterRouteComponent extends Route {
  render?: undefined
  component: string
  chldren?: undefined
}

interface BetterRouteChildren extends Route {
  render?: undefined
  component?: undefined
  Children: string
}

type BetterRoute =
  | BetterRouteRender
  | BetterRouteComponent
  | BetterRouteChildren

const invalidRouteBecauseEmpty: BetterRoute = {}
const invalidRouteBecauseDupe: BetterRoute = { render: 'render', component: 'component' }
const validRoute: BetterRoute = { render: 'render' }

Итак, два вопроса:

  1. Есть ли лучший способ справиться с вышеперечисленным?
  2. Если нет, я ищу помощь, пытаясь написать такой тип:
type BetterOptionalTypeConstructor<T, KS extends Array<keyof T>> = unknown

... который использовал бы что-то вроде BetterOptionalTypeConstructor<Route, ['render', 'component', 'children']>, и выплюнул бы тип BetterRoute, показанный выше.

Я пока не приложил много усилий, но похоже, что Typescript ненасколько я могу судить, в настоящее время не поддерживает "типы сопоставленных объединений".Любое понимание этого фронта будет оценено!

1 Ответ

0 голосов
/ 27 сентября 2019

Проведя некоторые исследования по ссылке @Mickey, я нашел следующее решение.В частности, это обсуждение на Github было очень поучительным.

/**
 * @typedef Without
 *
 * Takes two record types `T` and `U`, and outputs a new type where the keys
 * are `keyof T - keyof U` and the values are `undefined | never`.
 *
 * Meant to be used as one operand of a product type to produce an XOR type.
 */

type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/**
 * @typedef XOR
 *
 * Takes two record types `T` and `U`, and produces a new type that allows only
 * the keys of T without U or the keys of U without T.
 */
type XOR<T, U> = (T | U) extends object
  ? (Without<T, U> & U) | (Without<U, T> & T)
  : T | U;

// XOR is associative so we can compose it
type BetterRouteProps = RoutePropsCommon &
  XOR<RoutePropsComponent, XOR<RoutePropsRender, RoutePropsChildren>>;

// TypeError: missing properties
const invalidRouteMissingProps: BetterRouteProps = {};

// TypeError: too many properties
const invalidRouteTooManyProps: BetterRouteProps = {
  render: () => <div>Hello</div>,
  component: () => <div>Hello</div>
};

// Valid!
const validRoute: BetterRouteProps = {
  render: () => <div>Hello</div>,
  component: undefined // this is allowed
};

Я написал более полный анализ здесь , если кому-то интересно.

...