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
Exact object shapes lost after refinement #5280
Comments
@AugustinLF Thanks for the quick answer. As to your second example - shouldn't it be possible though? Imho if the inexact shape and exact one have the same defined properties - it should be possible to cast them, but that would ofc 'lose' some information for the rest of the program. However if the lost information is that it potentially could have other properties too and now Im assuring the type checker that it wont have - that's fine by me. While in fact, in the first snippet given by you, it seems that the exact object shape might not be lost there, it seems that the second if statement is still not able to refine things correctly here. When inspecting |
I think there are a few things going on here to consider. Flow is only showing you one of the types that are possible (the least specific). While it is "aware" that the type can be the exact version, it is possible that this function could be called from some outside source that Flow can not type check -- so it is showing you what it knows without a doubt is the "shape" of the object. So as @AugustinLF pointed out, it does actually correctly type the exactness of the object when you use it: That being said, there are definitely some bugs as well which have shown themselves in quite a few ways but all appear to be related. In @Andarist example - the issue that striked me as the biggest issue was that the last function call could not be inferred properly as being nothign but a We can see that refinement does not do anything at all with Unions. While you woudl think that each refinement would remove the matching values from the possible values, it actually does not. typeof check does not validate in objectsSo our first "bug" that this highlights is the fact that the typeof check does not actually qualify anything at all as it would seem. ( // $Works
if (val.id && typeof val.id === 'string') {
two(val);
return;
}
// $ExpectError
if (typeof val.id === 'string') {
two(val);
} typeof check bug examplerefinement can not understand function interruptionThe other bug that @Andarist shows (and the one that gets me a bit more than the others with his example is that refinement is not inferred later in the function. In the following example, even though we have refined out the other two possible combinations and no other possibility other than the export function refine(val: One | Two | Three) {
if (val.type === 'response') {
// $Works
(val: Object);
// $Works
(val: Three);
// $Works
three(val);
return;
}
// $Works
if (val.id && typeof val.id === 'string') {
two(val);
return;
}
// $Error
(val: One);
// $Error
(val: Two);
// $Works
(val: One | Two);
/*
property `id`
Property not found in
object type
*/
one(val);
if (val.id === undefined) {
// $Works
one(val);
}
} refinement bug exampleThere were two others that I had discovered with this but don't have the time to put together at the moment. I also tried making all the typing much more strict with no change: fully commented example w/ strict typing |
@bradennapier Thanks for putting those repros together! Great work, I've also learned a little bit from it about how to write flow repros. I've also tried adding additional checks, wrapping it into extra functions etc, cannot make it to refine this. However I've noticed that this might be some kind of a bug with refining through 2 distinct properties or something. const handleMessage = (message: SocketMessage) => {
if (message.type === 'response') {
handleResponse(message)
return
}
if (message.request_id === undefined) {
// $Works
(message: Push);
handlePush(message)
return
}
// $Error
(message: PushResponse);
handleResponse(message)
} but if we reverse this second refining logic, we end up with const handleMessage = (message: SocketMessage) => {
if (message.type === 'response') {
handleResponse(message)
return
}
if (message.request_id !== undefined) {
// $Error
(message: PushResponse);
handleResponse(message)
return
}
// $Works
(message: Push);
handlePush(message)
} |
@Andarist well you can definitely type it, it just seems to require some serious refinement. We personally implement this type of pattern by using Flow's opaque types now that eslint-plugin-flowtype supports opaque types (for most cases). They definitely can make life easier in handling these things. They work well with the validation handling, and by providing the opaque type there is really no chance you accidentally try to send an object that you weren't intending to. In the case below I use flow-type-transformer as a object transformation helper but return opaque types. Actually caught 3 bugs that weren't known about for the past 3 years by doing it too. It's a bit more boilerplate but I can't recommend it enough. // send function shape
// send = (data: Dash$ResponseWrapper): WebSocketSession => {}
export type Dash$ResponseWrapper =
| Dash$SuccessWrapper
| Dash$ErrorWrapper;
type SuccessWrapper = {|
result: 'success',
code: number,
uuid: ?string,
responses: Array<Dash$ResponseTypes>,
|};
export opaque type Dash$SuccessWrapper: SuccessWrapper = SuccessWrapper;
type ErrorWrapperShape = {|
result: 'error',
code: number,
uuid: ?string,
errors: Array<string>,
|};
export opaque type Dash$ErrorWrapper: ErrorWrapperShape = ErrorWrapperShape;
type SuccessResponse = {|
+result: 'success',
+code: number,
+uuid: string,
+path?: ?string,
+data?: void | Route$SuccessResponseTypes,
|};
export opaque type Dash$SuccessResponse: SuccessResponse = SuccessResponse;
type ErrorResponseShape = {|
+result: 'error',
+code: number,
+uuid: string,
+path?: ?string,
+errors: Array<string>,
|};
export opaque type Dash$ErrorResponse: ErrorResponseShape = ErrorResponseShape;
export const formatErrorResponse: $Transform<
$Shape<{ ...Dash$ErrorResponse }>,
Dash$ErrorResponse,
> = createTransformer(transformErrorResponse);
export const formatSuccessResponse: $Transform<
$Shape<{ ...Dash$SuccessResponse }>,
Dash$SuccessResponse,
> = createTransformer(transformSuccessResponse);
export const formatResponseWrapper: $Transform<
// we accept the shape of either wrapper -- but require result is provided in minimum
$Shape<{ ...Dash$SuccessWrapper }> | $Shape<{ ...Dash$ErrorWrapper }>,
Dash$ResponseWrapper,
> = createTransformer(transformResponseWrapper); |
Hm, I've studied your In my example corrected by you and in your example given above I see you are using read only properties, do they give any extra guarantees in those situations or is just a matter of your preference? I suppose the core of your extra refinements lies in the fact that you define non-existent prop on those shapes, just so both props are there on any of the disjoint types, even if some is always undefined and that makes flow easier to understand this situation, right? |
I think I'm running into this issue in this example. I wanted to handle results from a form that will either be an update if an type Create = {| id?: void, field: string |} to get this to work, but I'm not sure why and if I'm missing something. |
I think this is somewhat buggy behaviour - flow seems to be able to refine disjoint types only when sentinel is present in all types. That's why you have to put it in the |
I just wrote my own simple repro on flow 0.65 identifying it as a bug with type-at-pos, and in testing it on tryflow, noticed it no longer happens on 0.66 0.66 changelog includes
|
When inspecting this repro's "bottom gutter" we can see that:
{|type: 'push'|} | {|request_id: string, type: 'response'|} | {|request_id: string, type: 'push'|}
- all have exact shapes{request_id: string, type: 'response'}
{type: 'push'} | {request_id: string, type: 'push'}
The text was updated successfully, but these errors were encountered: