Skip to content

Optional Chaining and Type Narrowing #33736

Closed

Description

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.

Activity

  1. itsMapleLeaf commented on Oct 6, 2019

    @itsMapleLeaf

    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

  2. Kingwl commented on Oct 7, 2019

    @Kingwl
    Contributor

    @kingdaro @zhongliangwang
    Seems fixed in #33821

  3. itsMapleLeaf commented on Oct 9, 2019

    @itsMapleLeaf

    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

  4. HoldYourWaffle commented on Oct 9, 2019

    @HoldYourWaffle
    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.

  5. ryanquinn3 commented on Oct 16, 2019

    @ryanquinn3

    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;
    }
  6. Kingwl commented on Oct 17, 2019

    @Kingwl
    Contributor

    Ryan Quinn (@ryanquinn3)

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

    That seems expected behavior

  7. ahejlsberg commented on Oct 18, 2019

    @ahejlsberg
    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 ?..

  8. ahejlsberg commented on Oct 18, 2019

    @ahejlsberg
    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

Metadata

Metadata

Labels

DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions