Skip to content

Don't allow explicit undefined fields on unions that don't have an undefined caseΒ #56398

@Tsmith18256

Description

@Tsmith18256

πŸ” Search Terms

union, undefined, keys, non-shared, properties

βœ… Viability Checklist

⭐ Suggestion

I think unions should enforce that keys can't be assign undefined if neither half of the union accepts it. If I can't assign an object to either interface, I wouldn't expect to be able to assign it to the union of the 2.

As an example, let's say I have these 2 interfaces:

interface IForumPost {
  id: string;
  title: string;
  description: string;
}

// A new forum post doesn't have an ID yet until it is persisted in the database
interface INewForumPost extends Omit<IForumPost, 'id'> {}

I don't have a scenario where the id field should be allowed to exist, but be undefined. Both of these object declarations will throw an error:

const post: IForumPost = {
  id: undefined, // error, needs to be a string for this interface
  title: 'Hello, world!',
  description: 'This is a forum post'
};

const post: INewForumPost = {
  id: undefined, // error, obj doesn't exist on this interface
  title: 'Hello, world!',
  description: 'This is a forum post'
};

The problem I had is that id can be undefined if I have a union of these 2:

const post: IForumPost | INewForumPost = {
  id: undefined, // TypeScript is fine with this
  title: 'Hello, world!',
  description: 'This is a forum post'
};

The part that had this lead to a bug in my code is that TS then doesn't let me simply do an if (post.id) or if (typeof post.id === 'string') check to determine which side of the union I have (those checks would be safe here), but it makes me do if ('id' in post) check (which is not safe here because it's true for undefined).

πŸ“ƒ Motivating Example

Let's say I have a function that tries to route the changes correctly to either "update" or "create":

const saveForumPost = (post: IForumPost | INewForumPost) => {
  if ('id' in post) {
    updateForumPost(post);
  } else {
    createForumPost(post);
  }
};

And I am creating this post object using a form that works for both editing or creating:

const postFromFormData: IForumPost | INewForumPost = {
  id: isNew ? formData.id : undefined,
  title: formData.title,
  description: formData.description
};

That id assignment is a pretty innocent ternary, but actually should not be accepted. saveForumPost now treats this as an existing entry and attempts to update instead of create. This change would require me to either provide a proper value for id or omit the key completely, leading to safer code.

πŸ’» Use Cases

1. What do you want to use this for?

I would like to use this to ensure stronger type safety in my code.

2. What shortcomings exist with current approaches?

Current approaches allow assigning invalid objects to unions.

3. What workarounds are you using in the meantime?

I have fixed the code, as needed, but not until after my bug was found. The workaround for now is just to be extra careful in this type of situation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions