打字稿 - 如何结合联合和交集类型
Typescript - how to combine Union and Intersection types
我有以下组件:
export enum Tags {
button = 'button',
a = 'a',
input = 'input',
}
type ButtonProps = {
tag: Tags.button;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['button'];
type AnchorProps = {
tag: Tags.a;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['a'];
type InputProps = {
tag: Tags.input;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['input'];
type Props = ButtonProps | AnchorProps | InputProps;
const Button: React.FC<Props> = ({ children, tag }) => {
if (tag === Tags.button) {
return <button>{children}</button>;
}
if (tag === Tags.a) {
return <a href="#">{children}</a>;
}
if (tag === Tags.input) {
return <input type="button" />;
}
return null;
};
// In this instance the `href` should create a TS error but doesn't...
<Button tag={Tags.button} href="#">Click me</Button>
// ... however this does
<Button tag={Tags.button} href="#" a="foo">Click me</Button>
为了能够提出这个问题,已经将其剥离了一点。关键是我正在尝试使用交叉类型来区分联合。我正在尝试根据标签值获得所需的道具。因此,如果使用 Tags.button
则使用 JSX 的按钮属性(并且上面示例中的 href
应该会产生错误,因为 button
元素不允许这样做) - 然而另一个复杂性是我想使用 a
或 b
,但它们不能一起使用 - 因此是交集类型。
我在这里做错了什么,为什么在添加 a
或 b
属性 时该类型只能按预期工作?
更新
我添加了一个带有示例的 playground,以显示何时应该出错以及何时应该编译。
您必须使 a 和 b 不是可选的,而是使用对应于包含 OR 的联合类型 (|)。它与非条件属性(从不)相交。所以现在 ts 可以正确区分它们:
更新
type ButtonProps = {
tag: Tags.button;
a?: string;
b?: string
} &
JSX.IntrinsicElements['button'];
这里是playground.
通常我使用以下技术来创建通用功能组件,但它也适用于您的情况。诀窍是将组件声明为 function
而不是 const
以便能够使其通用。这允许道具是通用的,这反过来又允许你做:
export enum Tags {
button = 'button',
a = 'a',
input = 'input',
}
type Props<T extends Tags = Tags> = JSX.IntrinsicElements[T] & {
tag: T;
} & ({ a?: string; b?: never } | { a?: never; b?: string });
function Button<T extends Tags>({ children, tag }: Props<T>) {
if (tag === Tags.button) {
return <button>{children}</button>;
}
if (tag === Tags.a) {
return <a href="#">{children}</a>;
}
if (tag === Tags.input) {
return <input type="button" />;
}
return null;
}
// These should error due to href not being allowed on a button element
const a = <Button tag={Tags.button} href="#" a="foo">Click me</Button>
const b = <Button tag={Tags.button} href="#">Click me</Button>
// These should work
const c = <Button tag={Tags.button} a="foo">Click me</Button>
const d = <Button tag={Tags.button} b="foo">Click me</Button>
const e = <Button tag={Tags.button}>Click me</Button>
// This should error as `a` and `b` can't be used together
const f = <Button tag={Tags.button} a="#" b='a'>Click me</Button>
唯一的缺点是不能直接输入Button
组件,不能说function Button<T>
是一个React.FC<Props<T>>
,只能输入它的props和return类型。
检查playground here(我把你的例子留在底部)
编辑 我用你问题中的例子更新了代码并修复了 playground link
在您的示例中,有 2 个问题必须解决,并且都源于同一个 "issue"(功能)。
在 Typescript 中,以下内容有时无法正常工作:
interface A {
a?: string;
}
interface B {
b?: string;
}
const x: A|B = {a: 'a', b: 'b'}; //works
您想要的是明确地将 B 从 A 中排除,并将 A 从 B 中排除 - 这样它们就不会同时出现。
discusses the "XOR"ing of types, and suggests using the package ts-xor,或者自己写。这是那里的答案示例(ts-xor 中使用了相同的代码):
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
现在,我们终于可以解决您的问题了:
interface A {
a?: string;
}
interface B {
b?: string;
}
interface C {
c?: string;
}
type CombinationProps = XOR<XOR<A, B>, C>;
let c: CombinationProps;
c = {}
c = {a: 'a'}
c = {b: 'b'}
c = {c: 'c'}
c = {a: 'a', b: 'b'} // error
c = {b: 'b', c: 'c'} // error
c = {a: 'a', c: 'c'} // error
c = {a: 'a', b: 'b', c: 'c'} // error
更具体地说,您的类型将是:
interface A {a?: string;}
interface B {b?: string;}
type CombinationProps = XOR<A, B>;
type ButtonProps = {tag: Tags.button} & JSX.IntrinsicElements['button'];
type AnchorProps = {tag: Tags.a} & JSX.IntrinsicElements['a'];
type InputProps = {tag: Tags.input} & JSX.IntrinsicElements['input'];
type Props = CombinationProps & XOR<XOR<ButtonProps,AnchorProps>, InputProps>;
我有以下组件:
export enum Tags {
button = 'button',
a = 'a',
input = 'input',
}
type ButtonProps = {
tag: Tags.button;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['button'];
type AnchorProps = {
tag: Tags.a;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['a'];
type InputProps = {
tag: Tags.input;
} & ({ a?: string; b?: undefined } | { a?: undefined; b?: string }) &
JSX.IntrinsicElements['input'];
type Props = ButtonProps | AnchorProps | InputProps;
const Button: React.FC<Props> = ({ children, tag }) => {
if (tag === Tags.button) {
return <button>{children}</button>;
}
if (tag === Tags.a) {
return <a href="#">{children}</a>;
}
if (tag === Tags.input) {
return <input type="button" />;
}
return null;
};
// In this instance the `href` should create a TS error but doesn't...
<Button tag={Tags.button} href="#">Click me</Button>
// ... however this does
<Button tag={Tags.button} href="#" a="foo">Click me</Button>
为了能够提出这个问题,已经将其剥离了一点。关键是我正在尝试使用交叉类型来区分联合。我正在尝试根据标签值获得所需的道具。因此,如果使用 Tags.button
则使用 JSX 的按钮属性(并且上面示例中的 href
应该会产生错误,因为 button
元素不允许这样做) - 然而另一个复杂性是我想使用 a
或 b
,但它们不能一起使用 - 因此是交集类型。
我在这里做错了什么,为什么在添加 a
或 b
属性 时该类型只能按预期工作?
更新
我添加了一个带有示例的 playground,以显示何时应该出错以及何时应该编译。
您必须使 a 和 b 不是可选的,而是使用对应于包含 OR 的联合类型 (|)。它与非条件属性(从不)相交。所以现在 ts 可以正确区分它们: 更新
type ButtonProps = {
tag: Tags.button;
a?: string;
b?: string
} &
JSX.IntrinsicElements['button'];
这里是playground.
通常我使用以下技术来创建通用功能组件,但它也适用于您的情况。诀窍是将组件声明为 function
而不是 const
以便能够使其通用。这允许道具是通用的,这反过来又允许你做:
export enum Tags {
button = 'button',
a = 'a',
input = 'input',
}
type Props<T extends Tags = Tags> = JSX.IntrinsicElements[T] & {
tag: T;
} & ({ a?: string; b?: never } | { a?: never; b?: string });
function Button<T extends Tags>({ children, tag }: Props<T>) {
if (tag === Tags.button) {
return <button>{children}</button>;
}
if (tag === Tags.a) {
return <a href="#">{children}</a>;
}
if (tag === Tags.input) {
return <input type="button" />;
}
return null;
}
// These should error due to href not being allowed on a button element
const a = <Button tag={Tags.button} href="#" a="foo">Click me</Button>
const b = <Button tag={Tags.button} href="#">Click me</Button>
// These should work
const c = <Button tag={Tags.button} a="foo">Click me</Button>
const d = <Button tag={Tags.button} b="foo">Click me</Button>
const e = <Button tag={Tags.button}>Click me</Button>
// This should error as `a` and `b` can't be used together
const f = <Button tag={Tags.button} a="#" b='a'>Click me</Button>
唯一的缺点是不能直接输入Button
组件,不能说function Button<T>
是一个React.FC<Props<T>>
,只能输入它的props和return类型。
检查playground here(我把你的例子留在底部)
编辑 我用你问题中的例子更新了代码并修复了 playground link
在您的示例中,有 2 个问题必须解决,并且都源于同一个 "issue"(功能)。
在 Typescript 中,以下内容有时无法正常工作:
interface A {
a?: string;
}
interface B {
b?: string;
}
const x: A|B = {a: 'a', b: 'b'}; //works
您想要的是明确地将 B 从 A 中排除,并将 A 从 B 中排除 - 这样它们就不会同时出现。
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
现在,我们终于可以解决您的问题了:
interface A {
a?: string;
}
interface B {
b?: string;
}
interface C {
c?: string;
}
type CombinationProps = XOR<XOR<A, B>, C>;
let c: CombinationProps;
c = {}
c = {a: 'a'}
c = {b: 'b'}
c = {c: 'c'}
c = {a: 'a', b: 'b'} // error
c = {b: 'b', c: 'c'} // error
c = {a: 'a', c: 'c'} // error
c = {a: 'a', b: 'b', c: 'c'} // error
更具体地说,您的类型将是:
interface A {a?: string;}
interface B {b?: string;}
type CombinationProps = XOR<A, B>;
type ButtonProps = {tag: Tags.button} & JSX.IntrinsicElements['button'];
type AnchorProps = {tag: Tags.a} & JSX.IntrinsicElements['a'];
type InputProps = {tag: Tags.input} & JSX.IntrinsicElements['input'];
type Props = CombinationProps & XOR<XOR<ButtonProps,AnchorProps>, InputProps>;