Consider this:
// @flow
type Suit =
| "Diamonds"
| "Clubs"
| "Hearts"
| "Spades";
type Rank =
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
| "Jack"
| "Queen"
| "King"
| "Ace";
type Card = {
suit: Suit,
rank: Rank,
}
type DiamondCard = { suit: "Diamonds", rank: Rank };
let x : Card = { suit: "Diamonds", rank: 2 };
let y : DiamondCard = { suit: "Diamonds", rank: 2 };
let cardIdentity = (c: Card) : Card => {
return c;
}
cardIdentity(x);
cardIdentity(y);
/*
src/state/reducers/test.js:30
30: cardIdentity(y);
^^^^^^^^^^^^^^^ function call
4: | "Clubs"
^^^^^^^ string literal `Clubs`. Expected string literal `Diamonds`, got `Clubs` instead
20: type DiamondCard = { suit: "Diamonds", rank: Rank };
^^^^^^^^^^ string literal `Diamonds`
src/state/reducers/test.js:30
30: cardIdentity(y);
^^^^^^^^^^^^^^^ function call
5: | "Hearts"
^^^^^^^^ string literal `Hearts`. Expected string literal `Diamonds`, got `Hearts` instead
20: type DiamondCard = { suit: "Diamonds", rank: Rank };
^^^^^^^^^^ string literal `Diamonds`
src/state/reducers/test.js:30
30: cardIdentity(y);
^^^^^^^^^^^^^^^ function call
6: | "Spades";
^^^^^^^^ string literal `Spades`. Expected string literal `Diamonds`, got `Spades` instead
20: type DiamondCard = { suit: "Diamonds", rank: Rank };
^^^^^^^^^^ string literal `Diamonds`
*/
If "Diamonds":Suit
is correct, and type Card = { suit: Suit, rank: Rank, }
and given that object types are structural and not nominal, and a type type DiamondCard = { suit: "Diamonds", rank: Rank };
(totally matches Card
). Shouldn't any DiamondCard
be usable in place of a Card
because they have the same structure since "Diamond"
is an effective correct value for Suit
?
I understand why it could be proper to reject this premise with a type annotation on the definition of DiamondCard
, something like this: type DiamondCard = { suit: ("Diamonds":Suit), rank: Rank };
but of course that's not possible.
Is this a bug or am I missing something?
Note: The title sucks, feel free to change it to something more appropriate!
Great question.
Given types A
and B
where A <: B
and object types OA = { p: A }
and OB = { p: B }
, it is _not_ the case that OA <: OB
.
Because objects are mutable, the subtyping relationship between objects is property-wise _invariant_.
We're working on readonly properties, which would enable property-wise covariance.
So many things I dind't get there, but that's just my ignorance on the topic and the syntax you used I suppose. So does it make sense to leave this open while 1: I learn a bit and can keep up with the conversation, and 2: you release what you are working on and consider a possible way to approach this?
And, this now works on master with a small change:
type Card = {
+suit: Suit,
+rank: Rank,
}
Most helpful comment
Great question.
Given types
A
andB
whereA <: B
and object typesOA = { p: A }
andOB = { p: B }
, it is _not_ the case thatOA <: OB
.Because objects are mutable, the subtyping relationship between objects is property-wise _invariant_.
We're working on readonly properties, which would enable property-wise covariance.