Considering the width subtyping concept I would expect
type Foo = {
prop: string
}
to be a subtype of
type Bar = {
prop?: string
}
and the following code valid
function FooBar(foo: Foo): Bar {
return foo;
}
But apparently as of v0.45 flow does not work this way.
12: return foo;
^ object type. This type is incompatible with the expected return type of
11: function FooBar(foo: Foo): Bar {
^ object type
Although when Foo is destructured and returned as an object expression, the code does not produce any errors
function FooBarDestructured(foo: Foo): Bar {
const {prop} = foo;
return {
prop
};
}
Foo is not a subtype of Bar since objects are invariant. { prop?: string } is the same thing as { prop: string | void }. Invariance dictates that it must be both a supertype and a subtype / aka the same type. Therefore, the only type that an object with the prop property can have is exactly string | void.
Object width subtyping speaks to additional properties, not different types of the same properties.
Your last example does not error since you're returning a new object and no longer asserting that foo is both Bar and Foo.
Foo is not a subtype of Bar since objects are invariant.
Are they? As documentation says Objects are structurally typed.
So I expect instances of type Foo to be assignable to variables of type Bar. And instances of Bar not to be assignable to variables of type Foo.
And I expect the previous code to be as valid as
let foo: string = 'foo';
let bar: string | void = undefined;
bar = foo;
And the following code
function BarFoo(bar: Bar): Foo{
return bar;
}
to be as invalid as
let foo: string = 'foo';
let bar: string | void = undefined;
foo = bar;
And I'm actually not asserting any type relations with my code. I only expect the instances to be structurally compatible. Because I'm trying to solve more complex practical case. I have a deep data structure with optional properties at different levels. And I have a part of application where those properties must always exist and a part where that is not required. So I want to enforce one way data flow only from more strict to less strict part. And destructuring here is not an option.
BarFoo asserts that bar (input) is both type Bar and Foo (output). BarFoo is just the identity function: A => A.
Basically the same as this...
type Foo = {
prop: string
}
type Bar = {
prop?: string
}
declare var foo: Foo
declare var bar: Bar
foo = bar // <—— Not ok
This gives you the flexibility you need:
interface Foo {
prop: string
}
interface Bar {
prop?: string
}
function FooBar(foo: Foo): Bar {
return foo
}
I think you're misunderstanding the given examples.
Given the type definitions
type Foo = {
prop: string
}
type Bar = {
prop?: string
}
I expect the code
function FooBar(foo: Foo): Bar {
return foo;
}
to be valid.
I expect the code
function BarFoo(bar: Bar): Foo{
return bar;
}
to be invalid.
Because an object instance {prop: 'bang'} satisfies both Foo and Bar type definitions.
So the fact that I say that object {prop: 'bang'} is of type Foo should not make it incompatible with functions that expect objects of type Bar.
And the following code
function FooBar(): Bar {
const foo = {
prop: 'bang'
};
return foo;
}
should not become invalid only because I say that foo is now of type Foo.
function FooBar(): Bar {
const foo: Foo = {
prop: 'bang'
};
return foo;
}
Yeah, I misread your example and swapped Foo & Bar in my comment (deleted).
Even though a value can satisfy two different invariant types, Invariant<T> !<: Invariant<U> even when T <: U.
Here is an example of Covariant<T> <: Covariant<U> when T <: U:
type Foo = $ReadOnlyArray<string>
type Bar = $ReadOnlyArray<?string>
function FooBar(): Bar {
declare var foo: Foo
return foo
}
Flow doesn't allow aliasing because it is dangerous especially in a mutable setting:
/* @flow */
type Foo = {
prop: string
}
type Bar = {
prop?: string
}
declare var foo: Foo
declare var bar: Bar
bar = (foo: any) // do it!
bar.prop = undefined
foo.prop // says `string` but it's actually `undefined` :/
TypeScript works the way you suggest, but it's treacherous even if only immutable objects are being used:
type A = { a: string, b: number }
type B = { a: string }
type C = { a: string, b?: string }
declare var a: A;
declare var b: B;
declare var c: C;
b = a;
c = b;
c.b // says `string|undefined` actually a `number`
@mwalkerwells
But types are not invariant unless they are Exact object types.
Foo is a subtype of Bar because Bar can be expressed through Foo. Therefore
type Foo = {
prop: string,
otherProp: number
}
type Bar = {
prop?: string,
otherProp: number
}
function FooBar(foo: Foo): Bar {
return foo;
}
Can be rewritten to
type Foo = {
prop: string,
otherProp: number
}
type Bar = Foo | {
otherProp: number
}
function FooBar(foo: Foo): Bar {
return foo;
}
type Foo = {
prop: string,
otherProp: number
}
type Bar = {
prop: string,
otherProp: number
} | {
otherProp: number
}
function FooBar(foo: Foo): Bar {
return foo;
}
which remains semantically the same but validates properly.
@gcnew
I can not fully agree with this reasoning. I still would not say it's that dangerous. It's just more work for typechecker. As unwrapping string|undefined value into a string variable would still need to be guarded by run time type check. So there is no way you would end up with number value unless you shortcut with only a falsiness check.
Also the comparison with TypeScript is not applicable because it handles optional properties entirely different way. Even this code is valid in TS:
type A = { a?: string }
declare var a: A;
const s: string = a.a;
Foo is a subtype of Bar because Bar can be expressed through Foo
This is incorrect. Object types are invariant with respect to property types (including optional).
which remains semantically the same but validates properly.
This is also not true. Unions are not equivalent to optional properties, because you are now allowed to delete properties from a union.
In any case, what you want is _covariance_:
type Foo = {
prop: string,
otherProp: number
}
type Bar = {
+prop?: string // <-- here
}
function FooBar(foo: Foo): Bar {
return foo;
}
The purpose of a type system is to verify programming logic at compile time. I showed and example how the proposed logic is unsound in it's nature. Yes, you can add runtime checks to guard against cascading errors and undefined behaviour but that's not the point. Neither flow nor TypeScript does that and at least for TypeScript it's a stated non-goal.
The comparison with TypeScript is relevant. null and undefined are an implicit part of all types only if strictNullChecks is off. However, strictNullChecks is available for more than an year already and it's enabled on all new projects created by tsc --init. In Playground click Options -> strictNullChecks to switch it on.
@vkurchatkin
This is incorrect. Object types are invariant with respect to property types (including optional).
No that is not true. Object types are at least contravariant.
This is also not true. Unions are not equivalent to optional properties, because you are now allowed to delete properties from a union.
Could you please elaborate on this a bit more with examples? If your statement is true I don't see any reason for using optional properties at all. Because I literally mean type {prop: string} | {} when I write {prop?: string} and that is how I expect the latter to behave.
When I say a property is optional I expect the data structures which don't have that property to pass the type check if the rest of properties are compatible. Otherwise I would use {prop: string | void} explicitly or even {prop: ?string} if I accept nulls.
It makes absolutely no sense to me when a literal value {prop: 'foo'} passes the {prop?: string} type check but when annotated with type {prop: string} it does not.
Thanks for + notation. Haven't found that in docs. But it's still a bit different from what is advertised as "structural typing" for objects. Which kinda assumes covariance by default. Covariance as in "more strictly typed data structures are assignable to variables expecting less strictly typed data structures". Same way as string value is assignable to string | void value, string property should be assignable to string | void property.
Also amending type definitions is not always possible. So given types
type Foo = {
prop: string
}
type Bar = {
prop?: string
}
how do I write a function FooBar(foo: Foo) => Bar that would be valid in flow without any kind of destructuring?
Also please explain why bar.prop = foo.prop is a valid code while bar = foo is not, given the "structural typing" promise made by flow?
@gcnew
I mean you already need a runtime guard to refine a string|void value into string variable in flow. And it's enforced by flow.
let foo: string | void = 'foo';
let bar: string;
bar = foo; // error
if (typeof foo === 'string') {
bar = foo; // ok
}
if (foo !== undefined) {
bar = foo; // also ok but less strict
}
if (foo) {
bar = foo; // also ok but even less strict
}
I don't see why would you handle properties differently (as c.b in your example). To make your example absolutely type safe it is enough to just use stricter run time type check instead of falsiness or undefined check wherever you expect c.b to be string. And that check is needed irrespective of the current discussion and the types of values you are able to assign to variable c.
Btw, your first example does not validate in TS. So if your aim would be to sell TS as a better type system, you would succeed.
Covariance as in "more strictly typed data structures are assignable to variables expecting less strictly typed data structures"
This has nothing to do with covariance, though.
Also please explain why bar.prop = foo.prop is a valid code while bar = foo is not, given the "structural typing" promise made by flow?
The latter may not be valid, because foo is a mutable object, and type of bar might allow operations that are not allowed on foo. In your example Bar allows deleting prop while Foo doesn't. That's why Foo is not subtype of Bar.
@vkurchatkin you are confusing subtyping and subclassing(inheritance) relation.
Foo is a subtype of Bar because all values of type Foo satisfy the definition of type Bar and instances of Foo can be used in any context where Bar is expected. Similarly to string being subtype of string | void.
Type itself does not allow any operations. It's just a description of data structure. Operation defines which types it is applicable for.
delete is applicable to type Foo in flow:
type Foo = {
prop: string
}
declare var foo: Foo;
delete foo.prop;
So your argument about mutability doesn't stand and forbidding foo = bar does not introduce any added type safety.
Type itself does not allow any operations. It's just a description of data structure
This is factually incorrect. Type {} matches all objects, but doesn't allow property access.
delete is applicable to type Foo in flow:
@vkurchatkin I'm sorry but you are challenging dictionary definitions. Please first consult wikipedia at least.
Type {} is a supertype of all object types. Which makes it possible to use values of all object types where {} is expected. However you can not use {} where a specific object type is expected. Property access or property deletion operations do not change these facts.
No matter how many operations I define for subtypes, they can still be safely used wherever a supertype is expected.
Now whether a particular type allows property access or not is a definition of property access. You could say that property access is allowed only on objects of type where that property is defined (flow, TS). Or you could say that it is allowed on value of any type except null and undefined (as in vanilla JS). This would not influence type relations. Same goes for property deletion.
I don't think that this discussion is constructive any more. You have been give the answer by 3 different people, furthermore, this issue is a duplicate of many others that you can find. Closing.
Well, thank you for promoting TypeScript then.
You did not answer the practical question though.
Given types
type Foo = {
prop: string
}
type Bar = {
prop?: string
}
how do I write a function FooBar(foo: Foo) => Bar that would be valid in flow without destructuring or fallback to any?
how do I write a function FooBar(foo: Foo) => Bar that would be valid in flow without destructuring or fallback to any?
You can't, because that would be unsafe.
@InvictusMB You can if you declare the property of Bar as covariant (i.e. readonly). See https://github.com/facebook/flow/issues/3866#issuecomment-299460405 again.
@vkurchatkin
But that's impractical. I have a part of application where Foo is expected and part where Bar is expected. And I need static type check to ensure that values from less strict environment do not go to more strict environment. Same way as I would for example enforce number values to be used where number | string are applicable but not the other way around.
That is a practical use case and for many JS projects the only reason to employ a static type checker. Because added value of static type checking compared to vanilla JS is all about enforcing contracts between application parts.
And if you are stating that my use case is not possible to implement in flow I have to look for a better suited type system. Which is TS so far.
@gcnew Yes, that is an option. As well as redefining optional properties via union types.
But that is not a viable solution in large scale applications where parts are developed by different teams.
As whenever I would need to introduce more strict type Foo I have to ask people maintaining type Bar to make optional properties covariant.
Or introduce a convention of always using covariant optional props or unions.
TypeScript is actually the inferior type system in this case. Flow is more strict and safe. I like its behaviour better, although I use TS daily for other reasons.
Neither is better in this case. According to @vkurchatkin flow is not applicable to the use case at all.
And I'm no longer confident if I should use flow. Because it has weird semantics with regard to subtyping.
I am going to repeat myself but when I define a type with optional property I imagine that it is a set of values with that property together with a set of values without that property. That is a literal definition of optional.
Now let us switch to a more simple concept: a union. As there it becomes even more absurd.
Given the type definitions
type Foo = { prop: number }
type Bar = { prop: string }
type FooBar1 = { prop: string | number }
type FooBar2 = { prop: string } | { prop: number }
I would assume FooBar1 and FooBar2 both to be supertypes of Foo and Bar. That is what I expect when I am promised a "structural typing".
Well... Uhmm... Flow. TypeScript.
It's good that you brought that up, because it's very easy to show why this is unsafe:
type Foo = { prop: number }
type FooBar = { prop: string | number }
var f: Foo = { prop: 1 };
var fb: FooBar = f;
fb.prop = 'x';
f.prop.toFixed(); // runtime error
Flow doesn't allow this, TypeScript does.
I do get your point and I see a big picture of why this is unsafe.
Flow considers these types incompatible because otherwise when the same reference is used in different contexts a mutation valid in one context could compromise type safety of that reference in other contexts. However such type system can no longer be advertised as "structural typing" as it becomes predominantly "nominal typing".
On a more practical note this prevents a certain set of programs from being statically typechecked in flow at all. As transitions from { prop: number } to { prop: string | number } and back are demanded by actual use cases. And because if both contracts are imposed by external systems there is no way to statically validate such transition in flow. So I would rather use a "less safe" TypeScript option than a typecast via any in flow.
That is not the case that I want to promote TypeScript in any way but I have to pick a tool that suits practical needs. And "less safe" is in quotes because if one is a JS developer and he considers using a static type checker he is most likely familiar with the concept of purity and dangers of side effects and mutability. So added value of flow strictness for him would be close to 0. Thus given the options it makes more sense to employ "less safe" TypeScript check in 100% of cases than "more safe" flow check in 95% and fall back to any in 5% of cases where flow tries to outsmart you.
Although the theory sounds nice and no matter how strict flow type system is if you don't approach side effects consciously and only rely on a type checker things will explode in your face in real life.
Most helpful comment
I do get your point and I see a big picture of why this is unsafe.
Flow considers these types incompatible because otherwise when the same reference is used in different contexts a mutation valid in one context could compromise type safety of that reference in other contexts. However such type system can no longer be advertised as "structural typing" as it becomes predominantly "nominal typing".
On a more practical note this prevents a certain set of programs from being statically typechecked in flow at all. As transitions from
{ prop: number }to{ prop: string | number }and back are demanded by actual use cases. And because if both contracts are imposed by external systems there is no way to statically validate such transition in flow. So I would rather use a "less safe" TypeScript option than a typecast viaanyin flow.That is not the case that I want to promote TypeScript in any way but I have to pick a tool that suits practical needs. And "less safe" is in quotes because if one is a JS developer and he considers using a static type checker he is most likely familiar with the concept of purity and dangers of side effects and mutability. So added value of flow strictness for him would be close to 0. Thus given the options it makes more sense to employ "less safe" TypeScript check in 100% of cases than "more safe" flow check in 95% and fall back to
anyin 5% of cases where flow tries to outsmart you.Although the theory sounds nice and no matter how strict flow type system is if you don't approach side effects consciously and only rely on a type checker things will explode in your face in real life.