TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Union type on object's property behaves different than having a union of the whole object with all possible values for this field

Open Llois41 opened this issue 11 months ago • 2 comments

🔎 Search Terms

union inference

🕗 Version & Regression Information

I use v5.7.3 and never tried it out before (but sanity-checking against random older versions does not seem to resolve the issue)

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/KYDwDg9gTgLgBDAnmYcDyAjAVsAxjANQEMAbAV2AGcAeAFQD44BeOWgbQGthEIAzVgLoBuAFAjQkWHFwQAdpXgBxAJYwAMkQwARYDCLKSwACYBZYFADmwAMp6YZSszgBvEXDgmAogCVFngIIAQmqeAFxwAOQAtuZWmoYRADRucABynp5aAPrenoH+1mGRssDGWVDAGESUwEkiAL5w1dJyCqIS0PBIKHAq6po6egbGZpY2dg5OmDj4xORU1N3AfL2qGtq6+oamseNE9pT0ouLgnQjIqKNW3sAAjhQKTq7uykbhClDKshai7jFjWQU+wc4T660GWxGu1swMoonqxw6UiWHl2N3uVBgACYnilXu8YJ9vr84P8rICJpRwksVmCBpthjsxjCDgA6dKZHJ5AqeEmNAA+LjxbzgHy+PxSZOAFNh1IutLW9KG2yuezZXl8AWCvIaYl4ZFk+GUcjgRggtjIvF4AAoolBwqr0Q8YABKIUvG121lSmUHZhMFh0jbKqHMynsjLZXL5Qpu57uBMVexQWRNMDKADCpBItqgLr5upE+sNMGNqbNFqtWNzDrRd2dWLjeM9UG9u19kwDgcVwchTKsLIcEc50Z5TYTid0ZBTacz2dz+ZS9ULxaNJqI6azJBzdtrYydmLgADIXD6gQc5SgFf1e4zVYPKMOo9zCnB6nH6kA

💻 Code

export type ObjectValues<T> = T[keyof T];

export const GitLabDetailedMergeStatus = {
  MERGEABLE: 'mergeable',
  NEED_REBASE: 'need_rebase',
} as const;
export type GitLabDetailedMergeStatus = ObjectValues<typeof GitLabDetailedMergeStatus>;

export type MergeRequest = {
  id: string;
  merge_status: GitLabDetailedMergeStatus;
};

export type MergeRequest2 = {
  id: string;
  merge_status: typeof GitLabDetailedMergeStatus.NEED_REBASE;
  } | {
  id: string;
  merge_status: typeof GitLabDetailedMergeStatus.MERGEABLE;
}

function doStuff(mr: MergeRequest) {
  if(mr.merge_status === GitLabDetailedMergeStatus.NEED_REBASE) {
      return apiCall(mr);
  }
}

function doStuff2(mr: MergeRequest2) {
  if(mr.merge_status === GitLabDetailedMergeStatus.NEED_REBASE) {
      return apiCall(mr);
  }
}

function apiCall(mr: MergeRequest & {merge_status: typeof GitLabDetailedMergeStatus.NEED_REBASE }) {}

🙁 Actual behavior

In doStuff() the function call throws an error saying Types of property 'merge_status' are incompatible. regardless of the check in the if statement above and also the correctly inferred type (if you hover over it or add the same if statement again (it tells you that this is useless because it always will be true)).

In doStuff2() there is no error.

🙂 Expected behavior

TypeScript should infer both types correctly when passing it to the function.

If I am misunderstanding something obvious here, please let me know.

Additional information about the issue

No response

Llois41 avatar Feb 17 '25 10:02 Llois41

This is working as intended. MergeRequest is not a union type that can be narrowed. Checking individual properties on a non-union type will not created new types.

MartinJohns avatar Feb 17 '25 11:02 MartinJohns

Ok, thanks for the quick answer.

I find this very confusing (from a developer experience standpoint) given the following (simplified) example:


function fun(str: 'bar') {}

function fun2(obj: {foo: 'bar'}) {}

function doStuff(obj: {foo: 'bar' | 'baz'}) {
  if(obj.foo === 'bar') {
    fun(obj.foo) // no error, 'bar' gets correctly inferred
    fun2(obj) // error, TypeScript knows for sure that `obj` here has the type {foo: 'baz'}
  }
}

I get that TypeScript must not create a new type for primitves since string already exists. But still I am confused where the limitation on objects here comes from. But maybe I have the wrong understanding or expectation on what TypeScript actually does in this situation. I'm fine with closing this MR and happy to read more in-depth about what's happing here :)

Llois41 avatar Feb 17 '25 12:02 Llois41

This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar Mar 22 '25 01:03 typescript-bot