使用TypeScript,可以创建一种类型,其中至少需要两个属性中的一个。有(至少)两种处理方法:using union types和a 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,以及最新的可用类型。
答案 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'),
};