Skip to content

new Map(Iterable[K, V]) with complex value type V can have arbitrary extra properties on each V #49500

Closed
@thisisrandy

Description

@thisisrandy

Bug Report

πŸ”Ž Search Terms

map from iterable

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "map". The issue exhibits on the nightly playground.

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

interface Foo {
  bar: string,
  baz: number
}

// no error, even though qux is not a valid property of Foo
let fooMap: Map<string, Foo> = new Map([["foo", {bar: "bar", baz: 1, qux: "qux"}]]);
// the subtractive case is caught
fooMap = new Map([["foo", {bar: "bar", qux: "qux"}]]);
// if the iterable is pulled out and typed, the error is caught. could
// also achieve this with a suffix of `as [string, Foo][]` above
let fooIterable: [string, Foo][] = [["foo", {bar: "bar", baz: 1, qux: "qux"}]]
// setting with invalid values is also caught
fooMap.set("bar", {bar: "bar", baz: 1, qux: "qux"})
// it's worth noting that primitive value types are checked as expected
let stringMap: Map<string, string> = new Map([["foo", 1]])

πŸ™ Actual behavior

identifier: Map<K, V> = new Map(Iterable[K, V]) allows each V in the iterable to have arbitrary extra properties.

πŸ™‚ Expected behavior

identifier: Map<K, V> = new Map(Iterable[K, V]) should check that each value in the iterable is a valid V.

Activity

MartinJohns

MartinJohns commented on Jun 11, 2022

@MartinJohns
Contributor

Objects are not sealed. They're allowed to have arbitrary extra properties. This is unrelated to Map<>.

You want #12936, but don't expect it to happen any time soon.


// no error, even though qux is not a valid property of Foo
let fooMap: Map<string, Foo> = new Map([["foo", {bar: "bar", baz: 1, qux: "qux"}]]);

You don't provide any type arguments for your Map<> creation, so they're inferred based on the arguments you pass. In this case the inferred type is Map<string, { bar: string; baz: number; qux: string }> . You assign this value to a variable typed Map<string, Foo>, which is compatible (the type { bar: string; baz: number; qux: string } is compatible with the type Foo).

You probably expect it to behave like #7782 suggests.

thisisrandy

thisisrandy commented on Jun 12, 2022

@thisisrandy
Author

Right, I think I've understood. Basically, it boils down to I can do this

interface Foo {
  bar: string,
  baz: number
}

const fooLike = { bar: "bar", baz: 1, qux: 1 };
const foo: Foo = fooLike;

But not this:

const badFooLiteral: Foo = { bar: "bar", baz: 1, qux: 1 };

The Object literal may only specify known properties... error was pushing me in the wrong direction on a still fuzzy (but now quite clear) point about structural typing.

If I may, do the docs cover this well? I would expect to see something highlighting the object literal case e.g. on the Type Compatiblity page, but I only see two step examples.

MartinJohns

MartinJohns commented on Jun 12, 2022

@MartinJohns
Contributor

I think I've understood. Basically, it boils down to I can do this

Yep, exactly.

The Object literal may only specify known properties... error was pushing me in the wrong direction on a still fuzzy (but now quite clear) point about structural typing.

It very often leads to this kind of misunderstanding. It should be considered more of a linter feature, not a type-safety feature.

If I may, do the docs cover this well? I would expect to see something highlighting the object literal case e.g. on the Type Compatiblity page, but I only see two step examples.

It's shown under "Starting out", the basic rule is mentioned there.
Otherwise:
image

thisisrandy

thisisrandy commented on Jun 12, 2022

@thisisrandy
Author

Great, thanks for your help. I see some stuff on e.g. TypeScript for JS Programmers that deals with misspelling of properties, but I'm not sure that properly nails home the point about extra properties when the required ones are already properly specified. I'll think on where a note about this might fit on the Type Compatibility page and submit a PR.

MartinJohns

MartinJohns commented on Jun 12, 2022

@MartinJohns
Contributor

Your example with "Foo" is literally the the second example on that page: https://www.typescriptlang.org/docs/handbook/type-compatibility.html#starting-out

interface Foo {
  bar: string,
  baz: number
}

const fooLike = { bar: "bar", baz: 1, qux: 1 };
const foo: Foo = fooLike;

interface Pet {
  name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;
thisisrandy

thisisrandy commented on Jun 12, 2022

@thisisrandy
Author

No, I see that. My point was that

// Object literal may only specify known properties, and 'qux' does not exist in type 'Foo'
const badFooLiteral: Foo = { bar: "bar", baz: 1, qux: 1 };

was not highlighted. The example

interface User {
  name: string;
  id: number;
}
 
const user: User = {
  username: "Hayes",
  // Type '{ username: string; id: number; }' is not assignable to type 'User'.
  // Object literal may only specify known properties, and 'username' does not exist in type 'User'.
  id: 0,
};

here is basically the same thing, but that may not be obvious to new users learning the core concepts, since a misspelled property doesn't necessarily feel the same as an extra property.

Here's what happened to me:

  1. I thought I understood that an object is assignable to a type as long as the object is a weak structural superset of the type (or any).
  2. The Object literal may only specify known properties... error made me second-guess myself, because naively, it seemed the same as (1).
  3. I was unable to understand why let fooMap: Map<string, Foo> = new Map([["foo", {bar: "bar", baz: 1, qux: "qux"}]]); was fine because the logic of assignability had gotten fuzzed.

Hence, I want to highlight the object literal assignment case somewhere clearly in the docs. If that's already done and I somehow missed it, please point it out, but the example you highlighted was absorbed on reading yet didn't help to keep me from confusing myself later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @MartinJohns@thisisrandy

        Issue actions

          new Map(Iterable[K, V]) with complex value type V can have arbitrary extra properties on each V Β· Issue #49500 Β· microsoft/TypeScript