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

$Required utility type #7571

Closed
wants to merge 10 commits into from
Closed

Conversation

goodmind
Copy link
Contributor

@goodmind goodmind commented Mar 19, 2019

Fixes #5134

It doesn't touch T | void unions

@goodmind goodmind changed the title Required type $Required utility type Mar 19, 2019
Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

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

@dsainati1 has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@dsainati1
Copy link
Contributor

This looks like a good approach, but I have a couple of concerns.

I'm not entirely convinced by how this utility type behaves with respect to class and instance types. You are able to cast something of type PointC to $Required<PointB>, which seems to violate the nominality classes are supposed to have. I think because you are producing an object type in the code where you handle instances, your check ignores the class hierarchy which I don't think is what we'd want. The code as written also only handles own props. Is there a good reason we'd want this utility to work on instances in the first place? It seems like this would be much simpler if we restrict it to object types.

Your provided tests also have some discrepancies in them: you add an error comment on
(pc: $Required<PointB>); // error, (ps: $Required<{ x?: number }>); // error, and (pi: $Required<{ x?: number }>); // error but no error is reported in these places. Did you intend for there to be an error on these casts?

I'd also like to see a few tests that ensure that property variance is properly maintained by this utility.

@goodmind
Copy link
Contributor Author

goodmind commented Mar 19, 2019

@dsainati1 it converts classes to objects, just like $ReadOnly and $Rest does.
PointC to $Required<PointB> looks like bug
image

How can I seal object completely from flowing?

Copy link
Contributor

@jbrown215 jbrown215 left a comment

Choose a reason for hiding this comment

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

Looking good overall, but I think you worked harder than you needed to.

If you move away from using a new use_t and instead implement $Required in object_kit (search for "and object_kit" in flow_js.ml) then you'll be able to reduce your changes quite a bit.

Polymorphism + type destructors is sometimes tricky, so please add a few tests to make sure $Required works in polymorphic functions.

@@ -7351,6 +7407,7 @@ and eval_destructor cx ~trace use_op reason t d tout = match t with
| ReadOnlyType ->
let open Object in
ObjKitT (use_op, reason, Resolve (Next), ReadOnly, tout)
| RequiredType -> RequiredT (reason, tout)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of adding a new use type, can you follow the way ReadOnly is implemented in object_kit and do the same? It will significantly simplify your code by handling a ton of edge cases for you.

For example, your code will error on mixed/empty, which the object_kit code can handle more elegantly.

You also won't need the IncompatibleRequiredT because object kit handles errors with types that can't be converted into an object.

You also shouldn't need to distinguish between instances and objects, since object_kit will take care of that for you.

Copy link
Contributor Author

@goodmind goodmind Mar 19, 2019

Choose a reason for hiding this comment

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

But object_kit doesn't have polarity, so it always becomes neutral

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also there is issue in object_kit with statics, maybe you know how to fix it?
#7572

newtests/Required/test.js Show resolved Hide resolved
@goodmind
Copy link
Contributor Author

goodmind commented Mar 20, 2019

I don't understand why contravariant properties get replaced with mixed in object_kit
https://github.com/facebook/flow/blob/master/src/typing/flow_js.ml#L10884-L10888

| None ->
      let reason = replace_reason_const (RUnknownProperty (Some x)) r in
      let t = DefT (reason, bogus_trust (), MixedT Mixed_everything) in
      t
    in

image

@goodmind goodmind requested a review from jbrown215 March 20, 2019 12:34
@@ -1981,7 +1982,7 @@ and Object : sig
(* A union type resolves to a resolved spread with more than one element *)
and resolved = slice Nel.t

and slice = reason * props * dict * TypeTerm.flags
and slice = reason * props * dict * TypeTerm.flags * Properties.t
Copy link
Contributor Author

@goodmind goodmind Mar 20, 2019

Choose a reason for hiding this comment

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

I added original props (raw_props) because I needed polarity from field structure, and also doesn't wanted to get mixed on write-only properties. Not sure if this belongs here, or perhaps object_kit can be more generic

match p with
| Field (_, DefT (_, _, OptionalT t), polarity) -> Field (None, t, polarity)
| Field (_, t, polarity) -> Field (None, t, polarity)
| Get (_, t) -> Get (None, t)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This handles getters/setters in object, but not in classes, not sure why

@@ -10985,15 +10988,16 @@ and object_kit =
Obj_type.sealed_in_op reason flags1.sealed &&
Obj_type.sealed_in_op reason flags2.sealed;
} in
reason, props, dict, flags
let raw_props = raw_props1 in
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably should merge original props also? But they don't get used here

@goodmind
Copy link
Contributor Author

goodmind commented Mar 20, 2019

Pick implemented with $Required

type RestUnsound<T1, T2> = $Rest<T1, $Required<T2>> // $Rest doesn't remove optional props

type Pick<T1, T2> = RestUnsound<
  T1,
  $ObjMap<RestUnsound<T1, T2>, <V>(V) => any>
>;

declare var picked: Pick<{| +a?: number, b?: string, c: string |}, {| a: any |}>;

picked.a; // void | number
picked.a = 1; // doesn't save polarity
picked.b; // error
picked.c; // error

Avoids this bug #5936
Edit: this isn't bug actually as per comment about Rest

If the object we are using to subtract has an optional property, non-own
property, or is inexact then we should add this prop to our result, but
make it optional as we cannot know for certain whether or not at runtime
the property would be subtracted.

@goodmind
Copy link
Contributor Author

@jbrown215 friendly ping here

@jbrown215
Copy link
Contributor

I can't make time for this yet. I'm spending all my time on a big hurdle in my spreads project. Once I get past that, this will be the first thing I review.

@goodmind
Copy link
Contributor Author

goodmind commented Mar 29, 2019

The more i think of this, probably there should be more general solution, i.e. some mapper or even $ObjMap itself should be allowed to configure property type descriptor (similar to js, but not identical)

type $PropertyDescriptor<T> = {|
   optional: boolean,
   enumerable: boolean, // ???
   value: T,
   polarity: 
     | '+' // positive 
     | '-'  // negative
     | 'N'  // neutral
|}

type $ReadOnly<T> = $ObjModify<
  T, 
  <Descriptor>(Descriptor) => {|...Descriptor, polarity: '+'|}
>
type $WriteOnly<T> = $ObjModify<
   T, 
   <Descriptor>(Descriptor) => {|...Descriptor, polarity: '-'|}
>
type $ReadWrite<T>  = $ObjModify<
  T, 
  <Descriptor>(Descriptor) => {|...Descriptor, polarity: 'N'|}
>

type $Required<T> = $ObjModify<
  T, 
  <Descriptor>(Descriptor) => {|...Descriptor, optional: false|}
>

I realize that type destructors are tricky, and not all of them work well with each other, so perhaps generalization would only make it worse

@dsainati1
Copy link
Contributor

So after chatting to some people on the team I think we would like to decrease the number of $ types in Flow's system, so I am going to close this since it is moving us in slightly different direction that we'd like. This can be equivalently implemented with $ObjMap in any case.

@dsainati1 dsainati1 closed this Apr 12, 2019
@goodmind
Copy link
Contributor Author

@dsainati1 this can't be implemented with $ObjMap

@goodmind goodmind added the Superseded PRs that solve the same issue as a "superseding" PR that already landed label Jul 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Superseded PRs that solve the same issue as a "superseding" PR that already landed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Convert Optional Type Properties to Required
4 participants