是否可以在多个属性是可选的但必须至少存在一个的情况下创建接口?

时间:2019-09-26 21:45:55

标签: typescript algebraic-data-types

使用联合类型时,通常最简单的做法是使用带标签的联合创建ADT。但是,有时这是不可能的。

一个典型的例子是React Router的<Route/>组件。这里有三个可选的道具:componentrenderchildren,但必须有1个,并且必须正确传递这3个中的1个。

为简单起见,我在这里简化了类型,但是当前发布的类型看起来像这样:

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 :(得分:0)

在@Mickey发布的链接上进行了一些研究之后,我想到了以下解决方案。具体来说,this Github discussion很有启发性。

/**
 * @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
};

如果有人感兴趣,我会写出更完整的分析书here