Skip to content
This repository was archived by the owner on Jul 23, 2021. It is now read-only.

throw Error when passing a instance of Record as a Record factory default values #195

Conversation

jdeniau
Copy link

@jdeniau jdeniau commented Nov 20, 2020

Fixes #127

@jdeniau jdeniau force-pushed the record-as-default-value-of-record-factory branch from 05e2298 to b3a640d Compare November 20, 2020 09:18
const fooInstance = Foo();
expect(() => {
Record(fooInstance);
}).toThrow(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(() => Record(fooInstance)).toThrowErrorMatchingSnapshot()). The problem with writing the error two places is that if it should ever get out of sync it's hard to tell whether the source of truth is the test code or the app code. Also if anyone rewrites a bunch of errors all at once the copy and pasting is a real pain vs just running npx jest -u

src/Record.js Outdated
@@ -33,6 +33,12 @@ export class Record {
constructor(defaultValues, name) {
let hasInitialized;

if (isRecord(defaultValues)) {
throw new Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we know it's a record then there's no reason we can't take the correct action right? The error is caused in this case by the use of Object.keys(). The original bug suggests calling toJS, which almost makes sense except that it's a deep transformation and could potentially mangle information about what is stored in the record. I'd say you should call defaultValues.toObject() here. That is implemented at the collection level, so if you just check using isCollection I think you'd solve a whole family of related issues. Of course some kinds of collections may have very weird results.

I think the best of all worlds would be this: check isCollection(defaultValues) then if !isKeyedCollection(defaultValues) throw an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@conartist6 The fact is that isCollection and isKeyed returns false as the implementation does check the existence of @@__IMMUTABLE_ITERABLE__@@ and @@__IMMUTABLE_KEYED__@@, which are undefined for Record instances.

I changed the code with toObject as you suggested to simply fix the original issue, as it seems to dangerous to check the default value in the Record method (for now at least as it may induce bugs for weird objects).

The fact is, that now you can not pass a Map as defaultValues (but it wasn't possible either).

As a matter of fact, the record defaultValues can be really weird, as it accepts everything where you can call Object.keys on (even if the typescript types only allow instances of Object).

Maybe I should still add an error like this

if (typeof defaultValues !== 'object' || defaultValues === null) {
  throw new Error(
    'Can not call `Record` with a non-object as default values. You may want to use a javascript object.'
  );
}

and then later check if it's has the toObject or toJS method as default values ? 🤔

What do you think ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point generally is this: we should prefer working over throwing an error when both are possible. I forgot that records are not collections, but it doesn't change my general thought. In your case of course you didn't mean to pass a record, but think about somebody who did mean to do that. If we can understand what they meant, why should we not just do it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with throwing on collections. But for the same reasons you took this up initially it would be nice to tell the user what they did wrong. Of course they wouldn't be in an infinite loop, but the results would definitely be surprising.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know what is the direction of immutable about this point. The two options are valid: either throw to avoid unexpected comportment or try to correct user code whenever possible. 🤷‍♂️

Apparently immutable 3.8 does not address neither one or another solution on this specific feature.

I finally did two things:

  • accept Record as a constructor
  • throw when the default values are not an object

This way, the 3.8 comportment still works as expected, and there is an explicit error message when we try to inject weird default values.

We could eventually manage some other selected types (like Map, which is really similar to a JS object)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I did a little bit more digging to see what the other Immutable data structures do. Many of them end up calling keyedSeqFromValue, indexedSeqFromValue, or seqFromValue (code) which throws a TypeError if they're not of a proper type. Based on that I'm fine with throwing an error on immutable instances and non-objects.

I would still prefer not implicitly converting the Record since I don't think it's clear what the author's intention is.

Thanks for sticking through these long conversations!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine this, but I do disagree with @Methuselah96. Yes this one person passed a record and it wasn't what he meant. But the purpose of records is to behave like objects. There happens to be this one flaw in their facade as regards Object.keys, but I don't think we want to save that guy from himself. If the program can be executed we can't fix programmer error at runtime... Typescript probably could have caught the problem though.

Copy link

@Methuselah96 Methuselah96 Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to further defend my position, but feel free to stop me when this has become counter-productive.

I can't think of a case where I would want to use a Record as the defaultValues for a new Record in any code I've written, and I would rather get an error (and probably uncover a bug) and be forced to call .toObject() on the Record to make my intention clear (if it wasn't a bug). I would prefer to be saved from myself in this scenario.

I do agree that records are to behave like objects, but they're more heavy-weight because you have to define the structure of the "object" before you can actually create the "object." That's why I have a hard time believing that someone would choose to use this more heavy-weight "object" in order to just use as default values for another heavy-weight "object."

And, yes, hopefully TypeScript could have caught the problem, but I'm thinking it would require some special type guards against Records?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree with you that a legitimate use is pretty unlikely. I'm giving my approval to what @jdeniau has got now.

@jdeniau jdeniau force-pushed the record-as-default-value-of-record-factory branch from 85bd1f3 to 0a8e75a Compare November 20, 2020 13:48
Copy link
Member

@conartist6 conartist6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working through it with me! Fix that minor issue and I'll approve.

@jdeniau
Copy link
Author

jdeniau commented Nov 20, 2020

@conartist6 done !

Thanks for your review !

@jdeniau jdeniau force-pushed the record-as-default-value-of-record-factory branch from 0d6bf6d to 55c7c1e Compare November 20, 2020 20:04
Copy link

@Methuselah96 Methuselah96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

@jdeniau jdeniau force-pushed the record-as-default-value-of-record-factory branch from b05b022 to 3d15061 Compare December 3, 2020 07:56
Copy link
Member

@conartist6 conartist6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for bearing with me. It looks great!

Copy link

@Methuselah96 Methuselah96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seconded!

@Methuselah96 Methuselah96 merged commit 9c7e5cb into immutable-js-oss:master Dec 3, 2020
@jdeniau jdeniau deleted the record-as-default-value-of-record-factory branch December 3, 2020 14:55
@jdeniau
Copy link
Author

jdeniau commented Dec 3, 2020

Great, thanks to you for your time 🎉

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"too much recursion" error when creating a Record type from an instance of another Record
3 participants