-
Notifications
You must be signed in to change notification settings - Fork 13k
Description
Suggestion
See playground, it would be really nice if TS count keep the discriminator in context for other properties coming from the same object.
π Search Terms
Spread, context, discriminating union
β Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
When an object is spreaded like const { type, foo, bar, ...rest } = props: A | B | C
, all properties keep the same context (= relation to props
object type - A | B | C
). So, whenever you would narrow based on any of the discriminating type, it would narrow other properties (foo
, bar
, ...rest
). It would simplify working with JSX where it matters when extra properties are passed to the underlying element, because the prop doesn't have to be supported by the DOM and you would get a warning.
π Motivating Example
See below.
π» Use Cases
See playground above, it is very common to have a discriminated union as some form of input and then pass only the rest of the object down to some handler, omitting the discriminator as it is no longer needed. In most cases, it is fine to pass extra property, but when used with JSX, it adds extra property that will be added to the DOM.
type AvatarProps = { type: 'image', src: string, className: string } | { type: 'initials', name: string, className: string }
const Avatar = ({ type, className, ...rest }: AvatarProps) => {
if (type === 'image') {
return <ImageAvatar className={cx("avatar", className)} {...rest} /> // We don't want to pass type here as it is not part of the ImageAvatar interface
}
if (type === 'initials') {
return <InitialsAvatar className={cx("avatar", className)} {...rest} />
}
throw new Error(...)
}
The current workaround is to use the whole props object instead and then omit the unwanted props
type AvatarProps = { type: 'image', src: string } | { type: 'initials', name: string }
const Avatar = (props: AvatarProps) => {
if (props.type === 'image') {
const { className, ...rest } = props
return <ImageAvatar className={cx("avatar", className)} {..._omit(rest, "type")} /> // We don't want to pass type here as it is not part of the ImageAvatar interface
}
if (props.type === 'initials') {
const { className, ...rest } = props
return <InitialsAvatar className={cx("avatar", className)} {..._omit(rest, "type")} />
}
throw new Error(...)
}
As you can see the DX could be nicer if the "context" that ...rest
belongs to the same object as type
and thus discriminating on type should make the ...rest
return the correct type.