This seems like it should work:
class Query {
prop1: string = 'foo';
prop2: number = 12;
}
type QuerySpec = {
prop1?: string,
prop2: number
}
let q = new Query();
(q: QuerySpec)
Cannot cast 'q' to 'QuerySpec' because string [1] is incompatible with undefined [2] in property 'prop1'.
https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjGBDAzrsARQFcBTAJwE8wBvVMMAB3LkYEYAuMXAF3IEsAdgHMwAXjAByKHDiSA3PSYtGAJi6DiAWwBGFcWDarFAX3Q9KjUkTJUAylYwG6DZqzYB+LrwEiANEpuahraeuSoZqgwpDxgAI4GgqRIJBSUABQAlIrpcVyp9o6ZqEA
The general reasoning is:
If S is a subtype of T, then it _must_ be the case that any
statement that is true for all instances of T must also be true
for all instances of S. This is the Liskov substitution principle.
Given a QuerySpec with a prop1 attribute, it is valid to delete
the prop1 attribute, because it is optional in QuerySpec.
Given a Query, it is not valid to delete the prop1 attribute,
because it is not optional in Query.
Therefore, Query cannot be a subtype of QueryType.
Note that this exact same framework provides the answer to your previous
question, #6884:
Here, S is Query, T is QuerySpec, and the statement is “can
delete prop1”.
In that issue, S is Array<Sub>, T is Array<Super>, and the
statement is “can insert an element that is of type Super but not
of type Sub”.
Thinking in terms of, “what does this type guarantee that I can do? when
Flow tells me that these types are incompatible, is it telling me that
I’m potentially breaking a guarantee?” will help you discover these
answers yourself.
Try it out on this question. Why does making prop1 on QuerySpec
either non-optional or read-only suffice to solve the problem?
(This particular case is somewhat annoying in that Flow _is_ protecting
you from a real problem, but in many other cases Flow completely ignores
the same problem. In particular: delete is totally unchecked in Flow;
you can delete anything from any object. But I’d still say that Flow is
correct: the fact that delete is unchecked simply means that the
programmer must prove that each delete is valid based on the locally
available type information. If Flow admitted the subtyping described in
this question, then any such proof could be unsound.)
In my actual use-case, the q: QuerySpec is a function argument rather than a cast, so I guess my confusion came from my mental model being that the query spec is simply "passed in" and won't be mutated, though of course JavaScript (and even flow, seemingly?) has no way of enforcing this. In other words, my intent by making it optional was "it may or may not be there", not "it may be removed". Were there some way to mark the function argument as immutable, this example would presumably work because the property couldn't be deleted anyway, no?
To be honest I find my productivity constantly hampered by Flow's strictness. I appreciate the theoretical framework underpinning it, and why cases like this can't simply be allowed across the board, but I think this strong type system would become multiple times more useful overnight if it were given some config options to suppress errors for specific, common patterns like this. For those of us who want some type awareness but aren't writing Mars rover software, the signal to noise ratio is currently frustratingly low.
In my actual use-case, the
q: QuerySpecis a function argument
rather than a cast, so I guess my confusion came from my mental model
being that the query spec is simply "passed in" and won't be mutated,
though of course JavaScript (and even flow, seemingly?) has no way of
enforcing this. In other words, my intent by making it optional was
"it may or may not be there", not "it may be removed". Were there some
way to mark the function argument as immutable, this example would
presumably work because the property couldn't be deleted anyway, no?
I don’t understand. Flow absolutely lets you mark properties as
read-only. This is almost always what you want to do. You are correct
that it would solve the problem in this case, which I noted in my
answer. An analogous change also solves your previous issue #6884.
What am I missing?
For those of us who want some type awareness but aren't writing Mars
rover software, the signal to noise ratio is currently frustratingly
low.
I suspect that you’re just feeling the effects of the learning curve.
Designing programs is a skill, and it takes time to get a feel for it.
For me, the signal-to-noise ratio, in terms of “time saved due to Flow
catching real errors vs. time lost due to tracking down spurious
issues”, is easily at least 5:1. I asked a colleague, and they estimated
10:1 or higher. A lot of this comes from the _flexibility_ that a type
system affords you for performing large-scale changes. If you want to
change an API, or safely delete a function, or add a new kind of data to
a system, doing so with Flow is easy. You can just make the change, and
then Flow will tell you all the places that your software needs to be
updated to remain correct. Once you fix those places, your software
works. If you could “suppress” some errors, then these changes would no
longer be safe. Without a type system, we just wouldn’t feel comfortable
making these kinds of changes, or they would take much longer and
introduce many more bugs.
You don’t have to be writing software for a Mars rover to recognize that
it is critical that your software be _correct_.
Flow absolutely lets you mark properties as read-only.
Properties, yes, but in this case what I have is a mutable object that's being edited and constructed as part of the app state, which I then want to send off in an API call. Ideally what I'd like is to be able to mark a whole function argument as deeply-immutable, only within the scope of the function itself. It seems like marking the actual type's properties as immutable would interfere with their user-editing.
For me, the signal-to-noise ratio, in terms of “time saved due to Flow catching real errors vs. time lost due to tracking down spurious issues”, is easily at least 5:1. I asked a colleague, and they estimated 10:1 or higher.
When I first started using Flow the signal to noise ratio was more like 1:10. Now, after having spent a few weeks with it and having accepted quite a few compromises in terms of dry-ness - and having warmed to the idea that it's best to just ignore some of its errors as being wildly unhelpful - it's more like 1:1.
A lot of this comes from the flexibility that a type system affords you for performing large-scale changes. If you want to change an API, or safely delete a function, or add a new kind of data to a system, doing so with Flow is easy. You can just make the change, and then Flow will tell you all the places that your software needs to be updated to remain correct.
You don't need to sell me on the benefits of a type system; if I didn't already know and strongly desire them, I would've thrown my hands in the air and given up after the first few days of fighting with Flow.
Flow's documentation lays out the two ends of the type-system spectrum: Soundness and Completeness. This was a helpful explanation, and puts in context the frustration I've experienced in working with it. Every project has a different "sweet spot" on that spectrum at which it will most productively benefit from a type system. This is partially a product of scale, among other things. As explained in the docs, Flow very obstinately skews towards the soundness end of the spectrum. My qualm is with the fact that it would be really easy for the maintainers to add some config options that shift it more towards the completeness end of the spectrum - which would be much more productive for my project, and probably most projects - allowing developers to decide where on that line their project lies, and yet they refuse to.
Take for example, document.body. Flow assumes it to be a maybe-type, because technically it can be null. However, it's extremely easy to guarantee it won't be null by placing your script tag in the <body>. Flow doesn't understand this, though, so it insists that you make a check at every single usage of document.body, even though you can unequivocally vouch for its safety. This pointlessly complicates and obfuscates the code. I'm not saying it should be made non-maybe by default, I'm saying this is an ideal case for a config option, allowing an individual developer to say "I accept responsibility for this particular risk in exchange for cleaner code".
A non-exhaustive list of other things that a developer might reasonably want to opt-out of:
These are all cases where, in certain projects, the case being defended against might be unlikely (or tolerable) enough that the overhead of defending against it isn't worth the technical debt it introduces. That's a decision that should be left up to project developers, not one made unilaterally by Flow's maintainers.
Most helpful comment
The general reasoning is:
If
Sis a subtype ofT, then it _must_ be the case that anystatement that is true for all instances of
Tmust also be truefor all instances of
S. This is the Liskov substitution principle.Given a
QuerySpecwith aprop1attribute, it is valid to deletethe
prop1attribute, because it is optional inQuerySpec.Given a
Query, it is not valid to delete theprop1attribute,because it is not optional in
Query.Therefore,
Querycannot be a subtype ofQueryType.Note that this exact same framework provides the answer to your previous
question, #6884:
Here,
SisQuery,TisQuerySpec, and the statement is “candelete
prop1”.In that issue,
SisArray<Sub>,TisArray<Super>, and thestatement is “can insert an element that is of type
Superbut notof type
Sub”.Thinking in terms of, “what does this type guarantee that I can do? when
Flow tells me that these types are incompatible, is it telling me that
I’m potentially breaking a guarantee?” will help you discover these
answers yourself.
Try it out on this question. Why does making
prop1onQuerySpeceither non-optional or read-only suffice to solve the problem?
(This particular case is somewhat annoying in that Flow _is_ protecting
you from a real problem, but in many other cases Flow completely ignores
the same problem. In particular:
deleteis totally unchecked in Flow;you can delete anything from any object. But I’d still say that Flow is
correct: the fact that
deleteis unchecked simply means that theprogrammer must prove that each
deleteis valid based on the locallyavailable type information. If Flow admitted the subtyping described in
this question, then any such proof could be unsound.)