代码之家  ›  专栏  ›  技术社区  ›  Andrei V

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

  •  2
  • Andrei V  · 技术社区  · 7 年前

    至少一个 必须有两个属性。至少有两种方法: using union types a somewhat complexer generic solution . 这两种解决方案都很有效。当两个属性都没有指定时,我能够创建并获得所需的编译器错误。

    但是,当使用最终类型指定react组件中的属性时,编译器找不到两个“必需”属性。下面是一个简单的例子:

    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>'.
    

    有办法解决这个问题并让编译器理解这些需求吗?

    :使用TypeScript3.0.1,React 16.4.2,使用当前可用的最新类型。

    3 回复  |  直到 7 年前
        1
  •  2
  •   Titian Cernicova-Dragomir    7 年前

    您不能访问联盟中不是所有联盟成员共有的字段。

    想到的选项是类型守卫或使用不同类型的道具当你真正使用它们。

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

    这种新类型仍然与您原来的props兼容,因此我们只需要一个这种类型的局部变量并将其与类props一起分配。

    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
  •  1
  •   Laurence Dougal Myers    7 年前

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

    通过使用“type guard”并将呈现代码包装到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
  •  0
  •   Andrei V    7 年前

    其他答案中提出的使用类型保护可以满足编译器要求的共同思想是正确的,并且工作良好。但是,受union类型工作方式和问题中引用的答案的启发,我决定尝试构建一个不需要类型保护的泛型类型(请注意,尽管类型保护对于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 为每个指定的属性键入,将是完美的解决方案。我不知道现在是否可能。