Skip to content

Optional Chaining and Type Narrowing #33736

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

Closed
5 tasks done
arciisine opened this issue Oct 2, 2019 · 9 comments
Closed
5 tasks done

Optional Chaining and Type Narrowing #33736

arciisine opened this issue Oct 2, 2019 · 9 comments
Assignees
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@arciisine
Copy link

arciisine commented Oct 2, 2019

Search Terms

Optional Chaining

Suggestion

Currently I use a pattern of an in check with a ternary to narrow type on optional fields. With the new optional chaining, it would be great if the type narrowing could occur as the optional chain is progressed.

Use Cases

The primary use here is for type narrowing on union types without the need for extra boilerplate.

Examples

Current method of using the in operator for type narrowing.

type Header = { headers: Record<string, any> };
type Options = { config : any };

function getHeader(input: Header | Options, key: string, def?: any) {
   return ('headers' in input ? input.headers[key] : undefined) || def;
}

Improved syntax given optional chaining

type Header = { headers: Record<string, any> };
type Options = { config : any };

function getHeader(input: Header | Options, key: string, def?: any) {
   return input.headers?.[key] ?? def;
}

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@ghost
Copy link

ghost commented Oct 3, 2019

Thumb up to this issue. Type narrowing is not working with if blocks as well.

declare const foo: { bar: string, method: () => void } | undefined;

if (foo?.bar === 'SUCCESS') {
    foo.method();
}

Playground Link: https://www.typescriptlang.org/play/?ts=3.7-Beta#code/CYUwxgNghgTiAEYD2A7AzgF3gMyUgXPAN7wBGshmMAligOYA08AtiBgBZLCEAUAlPAC8APngA3JNWDwAvvAA+8AK4pQ2WiGABuAFA7q2eD1xIA-ADpyMIYMHwA5AGUAqgGFXAUUeP7AojvhAnDxzVg4ufl0ZIA

@itsMapleLeaf
Copy link

itsMapleLeaf commented Oct 6, 2019

Maybe this is also related. I ran into an issue with narrowing discriminated unions:

type Shape =
    | { type: 'rectangle', width: number, height: number }
    | { type: 'circle', radius: number }

declare const shape: Shape | undefined

const radius = shape?.type === 'circle' && shape?.radius

This errors on radius in shape?.radius:

Property 'radius' does not exist on type 'Shape'.
  Property 'radius' does not exist on type '{ type: "rectangle"; width: number; height: number; }'.(2339)

I would expect it know after the && that shape is a circle.

Playground

@Kingwl
Copy link
Contributor

Kingwl commented Oct 7, 2019

@kingdaro @zhongliangwang
Seems fixed in #33821

@itsMapleLeaf
Copy link

Found another similar problem: narrowing doesn't happen properly with switch

type Shape =
    | { type: 'rectangle', width: number, height: number }
    | { type: 'circle', radius: number }

function getArea(shape?: Shape) {
    switch (shape?.type) {
        case 'circle':
            return Math.PI * shape.radius ** 2
        case 'rectangle':
            return shape.width * shape.height
        default:
            return 0
    }
}

Playground

In the latest nightly as of this comment, this does typecheck if I refactor this to if statements

@HoldYourWaffle
Copy link
Contributor

I think I'm encountering a similar issue when using the new assertion statements:

declare function assertNonNull<T>(x: T | null): asserts x is T;
declare const x: { y: T | null } | null;

assertNonNull(x?.y); // Assert y, implicitly assert x
doSomething(x.y); // Error: x is possibly 'null'; the implicit assert for x didn't work

assertNonNull(x); // Explicitly assert x
doSomething(x.y); // No error anymore, both x and y have been narrowed down correctly

I'm using the latest nightly as of 9 october.

@ryanquinn3
Copy link

ryanquinn3 commented Oct 16, 2019

I've stumbled across what appears to be a similar issue to this one.

Heres a link to the playground. Playground is pointing to nightly but for record keeping I'm seeing this on 3.7.0-dev.20191016

The snippet:

type Feature = {
  id: string;
  geometry?: {
    type: string;
    coordinates: number[];
  };
};


function extractCoordinates(f: Feature): number[] {
    if (f.geometry?.type !== 'test') {
        // this case should be hit if f.geometry is undefined 
        return [];
    }
   // doesn't narrow f.geometry to the defined value
    return f.geometry.coordinates;
}

@Kingwl
Copy link
Contributor

Kingwl commented Oct 17, 2019

@ryanquinn3

// doesn't narrow f.geometry to the defined value
 return f.geometry.coordinates;

That seems expected behavior

@ahejlsberg ahejlsberg added Suggestion An idea for TypeScript and removed Bug A bug in TypeScript labels Oct 18, 2019
@ahejlsberg
Copy link
Member

We have previously discussed and rejected the notion that

input.headers && input.headers[key]

should be permitted, since that only establishes that input.headers is truthy, but not what its exact type is (there is no declaration of a headers property in Options which is effectively the same has having a headers of type unknown).

The exact same argument holds for input.headers?.[key] and for that reason we won't implement this suggestion.

You could argue this is also true for the in operator--and you would be right. See discussion in #10485. But this doesn't change the fact that we don't want this behavior for the very common operation of property access using . or ?..

@ahejlsberg
Copy link
Member

Meanwhile, several comments on this suggestion have pointed out valid issues relating to control flow analysis of optional chaining constructs. We'll track those in #34570.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants