【问题标题】:Require at least one of two properties to be provided in props要求在 props 中至少提供两个属性之一
【发布时间】:2019-01-24 03:52:34
【问题描述】:

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

但是,当使用 final 类型来指定 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,以及迄今为止可用的最新类型。

【问题讨论】:

    标签: reactjs typescript


    【解决方案1】:

    您不能访问非所有工会成员共有的工会字段。

    想到的选项是类型保护或在实际使用它们时使用不同类型的道具。

    简单的选项是类型保护,只需使用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
    

    【讨论】:

      【解决方案2】:

      您的“可识别”道具可以是“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;
            }
         }
      }
      

      【讨论】:

        【解决方案3】:

        其他答案中提出的共同想法,即使用类型保护可以满足编译器的要求,是正确的并且运行良好。但是,受联合类型如何工作以及问题中引用的答案的启发,我决定尝试构建一个不需要类型保护的通用类型(请注意,尽管 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"
        };
        

        一般的想法是在具有相同属性的两种类型之间建立一个联合类型。这避免了使用类型保护的需要。在这两种类型中的每一种上,都根据需要标记了一个或另一个属性。这需要在“base”接口中将这两个属性标记为可选。

        这个例子可以通用

        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 类型的通用类型将是完美的解决方案。我不知道目前是否可行。

        【讨论】:

          【解决方案4】:

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

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

          注意:自定义规则的属性名称是一次性的,它作为实际属性的用法是未定义的。

          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'),
          };
          

          【讨论】:

            猜你喜欢
            • 2019-05-15
            • 2021-08-02
            • 1970-01-01
            • 1970-01-01
            • 2014-04-09
            • 1970-01-01
            • 2012-01-07
            • 2022-07-27
            • 2017-07-05
            相关资源
            最近更新 更多