I have a ponent which renders different elements based on a specific property, called type
. It could have type definitions like this:
interface CommonProps {
size: 'lg' | 'md' | 'sm';
}
interface SelectInputProps extends CommonProps {
type: 'select';
options: readonly Option[];
selected: string;
}
interface TextInputProps extends CommonProps {
type: 'text';
value: string;
};
type InputProps = (SelectInputProps | TextInputProps) & ExtraProps;
function Field(props: InputProps): JSX.Element;
Now in my own ponent, I will access the properties of this ponent, like so:
import { ComponentProps } from 'react';
type FieldProps = ComponentProps<typeof Field>;
function MySpecialField(props: FieldProps) {
if (props.type === 'select') {
// this works
const { options, selected } = props;
}
return <Field {...props} />
}
This works absolutely fine. It knows in my if
block that props
is SelectInputProps
. However, I made one small change and it appeared to pletely break this mode of using the discriminated union.
type FieldProps = Omit<ComponentProps<typeof Field>, 'size'>;
In practice, here is what is happening
Why is this happening? Is there a way to fix it?
I have a ponent which renders different elements based on a specific property, called type
. It could have type definitions like this:
interface CommonProps {
size: 'lg' | 'md' | 'sm';
}
interface SelectInputProps extends CommonProps {
type: 'select';
options: readonly Option[];
selected: string;
}
interface TextInputProps extends CommonProps {
type: 'text';
value: string;
};
type InputProps = (SelectInputProps | TextInputProps) & ExtraProps;
function Field(props: InputProps): JSX.Element;
Now in my own ponent, I will access the properties of this ponent, like so:
import { ComponentProps } from 'react';
type FieldProps = ComponentProps<typeof Field>;
function MySpecialField(props: FieldProps) {
if (props.type === 'select') {
// this works
const { options, selected } = props;
}
return <Field {...props} />
}
This works absolutely fine. It knows in my if
block that props
is SelectInputProps
. However, I made one small change and it appeared to pletely break this mode of using the discriminated union.
type FieldProps = Omit<ComponentProps<typeof Field>, 'size'>;
In practice, here is what is happening
Why is this happening? Is there a way to fix it?
Share Improve this question asked Jun 1, 2021 at 18:36 corvidcorvid 11.2k12 gold badges71 silver badges134 bronze badges1 Answer
Reset to default 10It's because the Omit<T, K>
utility type does not distribute over union types in T
. The implementation uses keyof T
, and when T
is a union, keyof T
is only those keys that exist in all members of the union (keyof T
is contravariant in T
, so keyof (A | B)
is equivalent to (keyof A) & (keyof B)
). This is working as intended as per microsoft/TypeScript#31501.
Luckily, if you want a distributive version of Omit
, you can write it yourself using a distributive conditional type:
type DistributiveOmit<T, K extends PropertyKey> =
T extends any ? Omit<T, K> : never;
And then you can see the change in behavior:
type MyFieldProps = DistributiveOmit<React.ComponentProps<typeof Field>, 'a'>;
/* type MyFieldProps = Omit<{
type: 'select';
options: Option[];
} & ExtraProps, "a"> | Omit<{
type: 'text';
value: string;
} & ExtraProps, "a"> */
which makes your code start working:
function MyField(props: MyFieldProps) {
if (props.type === 'select') {
const options = props.options; // okay
}
return <input />
}
Playground link to code
function MyField(props: MyFieldProps) {
if (props.type === 'select') {
// Here's the problem
const options = props.options;
}
return <input />
}