Skip to content

An initialized binding element should be optional even if its parent is initialized too #2469

Closed

Description

function foo({ x, y = {}} = { x: 0, y: {}}) {

}

foo(); // Fine
foo({ x: 0, y: 0 }); // Fine
foo({ x: 0 }); // Error

The last line should not be an error because the y property has a default initializer. The error goes away if I remove the default initializer { x: 0, y: {}} on the parent.

I think the rule would be that even if we get the type of a parameter / variable / binding element from an initializer, we still have to traverse the binding pattern to determine whether binding properties are optional.

Activity

  1. added
    BugA bug in TypeScript
    SpecIssues related to the TypeScript language specification
    on Mar 23, 2015
  2. JsonFreeman commented on Mar 23, 2015

    @JsonFreeman
    ContributorAuthor

    Note that you can observe this in another way too. If you remove the y from the parameter initializer, then you get an error on the binding element instead of the call site. But it's really the same issue:

    function foo({ x, y = {}} = { x: 0 }) { // Error that { x: number } doesn't have a y property
    
    }
  3. ahejlsberg commented on Jul 23, 2015

    @ahejlsberg
    Member

    Mohamed Hegazy (@mhegazy) Curious why you removed the bug label?

  4. ahejlsberg commented on Jul 23, 2015

    @ahejlsberg
    Member

    Jason Freeman (@JsonFreeman) I don't think this is a bug. The order of precedence for determining the type of a variable or parameter is:

    1. Type annotation
    2. Initializer
    3. Type implied by binding pattern (if applicable)

    Consider this example:

    interface Foo { x: number, y: string }
    function f({ x, y = "hello" }: Foo) {
    }

    It would be odd indeed if the type of f's parameter isn't Foo as specified in the type annotation, but rather { x: number, y?: string } because y is magically made optional. I think the same logic applies to this example:

    interface Foo { x: number, y: string }
    var foo: Foo = { x: 0, y: "test" };
    function f({ x, y = "hello" } = foo) {
    }

    And, by extension, to this example:

    function f({ x, y = "hello" } = { x: 0, y: "test" }) {
    }

    Furthermore, if a parameter has a type annotation or an initializer, it's type shouldn't be impacted by whether the parameter itself is specified as a simple identifier or a binding pattern. The two should be interchangeable with no effect on the type. (Indeed, refactorings and/or the declaration file generator might depend on this invariant.)

    That said, we could potentially consider making properties optional in object literals if they are optional in the contextual type (which would apply here). I'm not really sure it's worth it, though.

  5. JsonFreeman commented on Jul 24, 2015

    @JsonFreeman
    ContributorAuthor

    It's funny. Reading your three examples, I actually think the first one should just be Foo, as you say. But in the second and third, y feels like it should be optional. The way I am thinking of it, a type annotation is a firm indication of the type. But an initializer, while it does specify the types of the binding elements, it doesn't tell you the whole story about optionality. I feel like it's not as strong an indicator of the type. And so when there is an intializer, but no type annotation, I feel like we do want to keep descending into the binding pattern to "optionalize" the elements that have their own initializers.

    Furthermore, if a parameter has a type annotation or an initializer, it's type shouldn't be impacted by whether the parameter itself is specified as a simple identifier or a binding pattern.

    Again, for a type annotation, I agree. For an initializer, I agree only if you converted the identifier to a binding pattern without adding initializers inside.

    As a side note, converting an identifier to a binding pattern may indeed affect the type because of tuples:

    (x = [0, 1, 2]) => { } // Array type
    ([x, y, z] = [0, 1, 2]) => { } // Tuple type
  6. DanielRosenwasser commented on Jul 24, 2015

    @DanielRosenwasser
    Member

    The order of precedence for determining the type of a variable or parameter is:

    1. Type annotation
    2. Initializer
    3. Type implied by binding pattern (if applicable)

    Anders Hejlsberg (@ahejlsberg) Is that actually the case? In the following example, param is contextually typed as a string, and the local variable x has the type (s: string) => number.

    var { x = (str: string) => +str } = { x: param => param.length };

    But if we used the initializer first, wouldn't we acquire the type of x from the type of x from the RHS of the top-level initializer?

    I feel like it's not as strong an indicator of the type.

    Yes, I see what Jason Freeman (@JsonFreeman) is saying - it would be kind of ideal if the properties "zipped up" with respect to optionality when determining assignability. I can definitely get behind a very simple rule, but I get that there's also something to be said about flexibility here.

  7. JsonFreeman commented on Jul 24, 2015

    @JsonFreeman
    ContributorAuthor

    Daniel Rosenwasser (@DanielRosenwasser) the contextual typing you are talking about is that the RHS gets contextually typed by the type implied by the binding pattern. However, the final type of x is determined by the outer initializer (which got contextually typed using the inner initializer).

    Anders Hejlsberg (@ahejlsberg) We could just do the contextual typing rule for optional properties like you mentioned. It would only work when the initializer is a literal though.

  8. ahejlsberg commented on Sep 1, 2015

    @ahejlsberg
    Member

    Jason Freeman (@JsonFreeman) Fix now available in #4598.

  9. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptFixedA PR has been merged for this issueSpecIssues related to the TypeScript language specification

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions