要求在道具中提供至少两个属性之一

时间:2018-08-17 06:40:55

标签: reactjs typescript

使用TypeScript,可以创建一种类型,其中至少需要两个属性中的一个。有(至少)两种处理方法:using union typesa somewhat complexer generic solution。这两种解决方案都能很好地工作。当两个属性都未指定时,我能够创建并获得所需的编译器错误。

但是,当使用最终类型在react组件中指定props时,编译器无法找到两个“必需”属性。这是一个简化的示例:

interface Base {
  text?: string;
}
interface WithId extends Base {
  id: number;
}
interface WithToken extends Base {
  token: string;
}
type Identifiable = WithId | WithToken | (WithId & WithToken);

//OK
const item: Identifiable = {
   text: "some text"
   id: 4
};
//OK
const item: Identifiable = {
   token: "some token"
};
//Error
const item: Identifiable = {
   text: "some text"
};

//OK
export class MyComponent extends React.Component<Identifiable, any> {
   render() {
      //Error (see below)
      return <p>{this.props.id}</p>
   }
}

尝试访问两个必需道具之一(无论类在何处)时收到的错误如下所示:

Property 'id' does not exist on type '(Readonly<{children?: ReactNode;}> & Readonly<WithId>) | (Readonly<{children?: ReactNode;}> & Readonly<WithToken>) | (Readonly<{children?: ReactNode;}> & Readonly<WithId & WithToken>)'
Property 'id' does not exist on type '(Readonly<{children?: ReactNode;}> & Readonly<WithToken>'.

是否可以解决此问题,并使编译器了解这些要求?

注意:使用TypeScript 3.0.1,React 16.4.2,以及最新的可用类型。

4 个答案:

答案 0 :(得分:2)

您不能访问该联合的所有成员都不通用的字段。

想到的选项是类型保护,或者在实际使用道具时使用其他类型的道具。

简单的选项是类型保护,只需使用in类型保护检查属性是否存在:

export class MyComponent extends React.Component<Identifiable, any> {
    render() {
        if ('id' in this.props) {
            // Ok
            return <p>{this.props.id}</p>
        }
    }
}

第二种更复杂的解决方案是从Identifiable创建一个类型,该类型是相同的并集,但是该并集的每个成员都增加了缺少的字段,形成了所有并集,其中缺少的字段是可选的,并且类型未定义。基本上,我们希望将表单{ id: string } | { token: string }转换为{ id: string; token?:undefined } | { token: string; id?:undefined }。这将使我们能够访问联合的任何字段,同时仍可以在strictNullChecks下进行安全键入。

此新类型仍将与您原来的道具兼容,因此我们只需要此类型的局部变量并将其分配给道具类即可。

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never


type FlattenUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<keyof TAll, keyof T>, undefined>> : never;
type FlattenUnion<T> = FlattenUnionHelper<T, UnionToIntersection<T>>


export class MyComponent extends React.Component<Identifiable, any> {
    render() {
        let props: FlattenUnion<Identifiable> = this.props
        return <p>{props.id}</p>
    }
}

我认为您可以将扁平化版本直接用作道具,但是从我测试的结果来看,您应该测试所有要求是否满足

export class MyComponent extends React.Component<FlattenUnion<Identifiable>, any> { /*...*/}
let f = <MyComponent id={0} />
let f2 = <MyComponent token="" />
let f3 = <MyComponent id={0} token="" />
let f4 = <MyComponent text="" /> // error

答案 1 :(得分:1)

您的“可识别”道具可以是“ WithToken”类型,它没有属性“ id”。编译器正确地将您的当前使用标记为错误。

通过使用“类型防护”来解决此问题,并将呈现代码包装在if语句中。

function isWithId(value: Identifiable): value is WithId {
   return (value as WithId).id != undefined;
}

export class MyComponent extends React.Component<Identifiable, any> {
   render() {
      if (isWithId(this.props)) {
         return <p>{this.props.id}</p>;
      } else {
         return null;
      }
   }
}

答案 2 :(得分:0)

在其他答案中提出的共同思想是,使用类型防护可以使编译器满意,这是正确的,并且效果很好。但是,受联合类型的工作方式和问题中引用的答案的启发,我决定尝试构建一个通用类型,该类型不需要类型保护就可以工作(请注意,尽管类型保护对于TypeScript编译器不是必需的) ,根据您的情况,仍然应该在运行时检查JavaScript对象中是否存在属性。

这是最终对我有用的解决方案:

interface Base {
   text?: string;
   id?: number;
   token?: string;
}

type RequireProperty<T, Prop extends keyof T> = T & {[key in Prop]-?:T[key]};
type RequireIdOrToken = RequireProperty<Base, "id"> | RequireProperty<Base, "token">;

//Error
const o1: RequireIdOrToken = { };
//Error
const o2: RequireIdOrToken = { 
   text: "some text"
};
//OK
const o3: RequireIdOrToken = { 
   id: 4
};
//OK -> no type guard needed
o3.token = "a new token";
//OK
const o4: RequireIdOrToken = { 
   token: "some token"
};

通常的想法是在两个具有相同属性的类型之间具有并集类型。这避免了使用类型保护的需要。在这两种类型的每一种上,一个或另一个属性都按要求标记。这要求在“基本”界面中将两个属性都标记为可选。

可以使示例通用化

type RequireProperty<T, Prop extends keyof T> = T & {[key in Prop]-?:T[key]};
type RequireTwoProperties<T, Prop1 extends keyof T, Prop2 extends keyof T> 
         = RequireProperty<T, Prop1> |  RequireProperty<T, Prop2>;

,因此扩展为接受多个属性。接受属性名称“列表”(keyof T的子集)并将其转换为每个指定属性的RequireProperty类型的联合类型的泛型类型将是理想的解决方案。我不知道目前是否可行。

答案 3 :(得分:0)

我为此编写了一个NPM模块:https://www.npmjs.com/package/react-either-property

该代码维护类型检查选项,提供EitherOptional和EitherRequired策略-它支持在一个道具定义中组合多种用法。

请注意:自定义规则的属性名称是一次性的,其实际属性的使用尚不确定。

import { EitherOptional, EitherRequired } from 'react-either-property';

    [ module code goes here] 

ComponentSeven.propTypes = {
  east: PropTypes.number,
  west: PropTypes.number,
  north: PropTypes.number,
  south: PropTypes.number,
  ignored: EitherOptional('north', 'south'),
  undefined: EitherRequired('north', 'south'),
};