Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional chaining should narrow union types too #56264

Closed
5 tasks done
mon-jai opened this issue Oct 30, 2023 · 5 comments
Closed
5 tasks done

Optional chaining should narrow union types too #56264

mon-jai opened this issue Oct 30, 2023 · 5 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@mon-jai
Copy link

mon-jai commented Oct 30, 2023

πŸ” Search Terms

optional chaining, union type

βœ… Viability Checklist

  • 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 our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

It should be allowed to access deep property of a union variable with optional chaining.

Consider the example below,

type Foo = { a: { b: string } } | { c: number }

declare let foo: Foo

// The following should be `string | undefined` but TypeScript failed to infer the type
foo.a?.b // Property 'a' does not exist on type '{ c: number; }'.

TypeScript should be able to infer foo.a?.b correctly, but currently TypeScript throws an error instead.

πŸ“ƒ Motivating Example

It would be useful for library users who want to access a property from a variable we don't have control over.

Here's an example with jscodeshift.

For TypeScript, it is so hard to work around with the type checker,

// root.find(jscodeshift.VariableDeclaration, variableDeclaration => {
  const variableDeclarator = variableDeclaration.declarations[0]
  if (variableDeclarator === undefined) return
  if (variableDeclarator.type !== "VariableDeclarator") return
  if (variableDeclarator.id.type !== "Identifier") return
  console.log(variableDeclarator?.id?.name)
})

For JavaScript, we can just,

root.find(jscodeshift.VariableDeclaration, variableDeclaration => {
  const name = variableDeclaration.declarations[0]?.id?.name
  if (name !== undefined) console.log(name)
})

I don't know if this is a bug instead.

#40397 was discussing the same issue, which was labeled as "fixed". However, the attached code snippet doesn't work on TypeScript v5.2.2.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 30, 2023

This is the intended behavior because object types aren't sealed. You can legally write this code

type Foo = { a: { b: string } } | { c: number }

let bar = { a: 0, c: 2 };
let foo: Foo = bar;

which then crashes if run (thus should have a type error)

foo?.a.toLowerCase();

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Oct 30, 2023
@ahejlsberg
Copy link
Member

Write the type like this instead:

type Foo = { a: { b: string } } | { a?: undefined, c: number }

Your scenario then works because you're ensuring that a will either be { b: string } or undefined.

@MartinJohns
Copy link
Contributor

@ahejlsberg Technically a can still be of an arbitrary type because it's optional in the second version.

@mon-jai
Copy link
Author

mon-jai commented Oct 31, 2023

Maybe this could be resolved if #12936 is implemented (some day).

@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Nov 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants