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

Change HashMap to export mapped types #339

Merged
merged 3 commits into from
Jul 5, 2024
Merged

Change HashMap to export mapped types #339

merged 3 commits into from
Jul 5, 2024

Conversation

gustavo-shigueo
Copy link
Collaborator

@gustavo-shigueo gustavo-shigueo commented Jul 5, 2024

Goal

Allow enums to be used as HashMap keys
Closes #338

Changes

Change HashMap to export { [key in K]?: V } instead of { [key: K]: V }

Checklist

  • I have followed the steps listed in the Contributing guide.
  • If necessary, I have added documentation related to the changes made.
  • I have added or updated the tests related to the changes made.

@gustavo-shigueo gustavo-shigueo merged commit 68f2f22 into main Jul 5, 2024
16 checks passed
@gustavo-shigueo gustavo-shigueo deleted the mapped_types branch July 5, 2024 17:47
@NyxCode
Copy link
Collaborator

NyxCode commented Jul 5, 2024

Interesting! How sure are we that

  • this is fully compatible with what we had before
  • there are no edge-cases with this?

There definitely are some semantic differences. Here are a couple I have found:

type A = { [key: string]: number };
let a: A = { "x": 42 };
let x: number = a["x"]; // ok

type B = { [key in string]?: number };
let b: B = { "y": 42 };
let y: number = b["y"]; // error
type A = { [key: string]: number };
let a: A = { "x": undefined }; // error

type B = { [key in string]?: number };
let b: B = { "y": undefined }; // ok

For just string keys, the first one (what we had before) gives a number for every string key. That might not be what one usually wants, though, and getting back a string | undefined does seem more sensible.

That does mean that this change is breaking, correct? Do we want to treat it as such, or is this more of a bug fix?

@gustavo-shigueo
Copy link
Collaborator Author

There definitely are some semantic differences. Here are a couple I have found:

type A = { [key: string]: number };
let a: A = { "x": 42 };
let x: number = a["x"]; // ok

type B = { [key in string]?: number };
let b: B = { "y": 42 };
let y: number = b["y"]; // error

I think this behavior is better because a HashMap tells you what type the value is, if the key exists, so this kinda translates HashMap::get to TS

type A = { [key: string]: number };
let a: A = { "x": undefined }; // error

type B = { [key in string]?: number };
let b: B = { "y": undefined }; // ok

This is annoying, and I don't know if there's a way to implement the feature without this behavior.
It is one of those TS quirks that can only be resolved by having the tsconfig flag for exactOptionalPropertyTypes
It should be harmless as far as (de)serialization is concerned though.

That does mean that this change is breaking, correct? Do we want to treat it as such, or is this more of a bug fix?

Yeah, this does look like a breaking change, it'll make TypeScript demand a whole lot of null checks when dealing with HashMap, but honestly, that's probably a good thing

@gustavo-shigueo gustavo-shigueo added the breaking change This PR will change the public API in some way label Jul 6, 2024
@SpecialMike
Copy link

This breaks iterating over object values:

//before
type A = {[key: string]: number};
let a: A = {"a": 1, "b": 2, "c": 3};
//el is a `number` below
let sum = Object.values(a).reduce((acc, el) => acc + el, 0);

//after
type A = {[key: string]?: number};
let a: A = {"a": 1, "b": 2, "c": 3};
//el is a `number|undefined` below
let sum = Object.values(a).reduce((acc, el) => acc + el, 0);

This does make sense that record types will return undefined on keys that aren't in them, but I think it's better handled by setting noUncheckedIndexedAccess in tsconfig, which doesn't cause this problem.

@NyxCode
Copy link
Collaborator

NyxCode commented Sep 29, 2024

Interesting!
We do still need the new representation for #349, but we could revert it back for other cases.

I do still think that the new representation is better overall, though I'd love to be convinced otherwise.
If you have a strong opinion on this, I'd appreciate if you could lay out the tradeoffs of the two representations for your usecase.

For one, I think we should chose the most sensible bindings with the default tsc config. I'd also err on the side of having false-positive tsc errors instead of false-negatives.

@gustavo-shigueo
Copy link
Collaborator Author

gustavo-shigueo commented Sep 30, 2024

You can give el a default value to make TS understand it's not undefined, also, if you're getting acc as number | undefined (you didn't say anything about it, but just in case), use <number> on the reduce call

type A = { [key in string]?: number }
const a: A = { a: 1, b: 2, c: 3 }

Object.values(a).reduce<number>((acc, el = 0) => acc + el, 0)

@SpecialMike
Copy link

SpecialMike commented Sep 30, 2024

You can give el a default value to make TS understand it's not undefined, also, if you're getting acc as number | undefined (you didn't say anything about it, but just in case), use <number> on the reduce call

Yeah, this trivial case is somewhat easy to work around, but if T is a non-primitive type there is not always an obvious default.

Is it possible to detect that the key type is enumerable and only output the ? in { [key in string]?: number } in that case? All other use cases seem to work well with just { [key in string]: number } - the value type is not mapped to undefined. The only downside with this is that if the key type is enumerable, then the value types are going to have | undefined, whereas other value types will not. If that's a problem, then maps with enumerable key types could be output as anonymous objects with the fields enumerated:

enum Keys {
  A,
  B,
  C,
}

struct A<T> {
  foo: HashMap<Keys, T>
}
type A<T> = {
  foo: {
    "A"?: T,
    "B"?: T,
    "C"?: T,
  }
}

This is essentially doing the type mapping by hand. I think this TS bug is related, and if fixed setting exactOptionalPropertyTypes would allow the optional mapped type to work: microsoft/TypeScript#46969

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change This PR will change the public API in some way
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Question to recent change Records<K, V> to { [key: K]: V }
3 participants