Skip to content

Keep context after using spreadΒ #53947

@alesmenzel

Description

@alesmenzel

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 typeand thus discriminating on type should make the ...rest return the correct type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions