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

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

Closed
thisisrandy opened this issue Jun 11, 2022 · 6 comments

Comments

@thisisrandy
Copy link

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.

@MartinJohns
Copy link
Contributor

MartinJohns commented Jun 11, 2022

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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
Author

thisisrandy commented Jun 12, 2022

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants