Assume we have two classes, which have a subtype relationship:
1
2
|
class Person { name: string }
class Employee extends Person { department: string }
|
{"value":"class Person { name: string }\nclass Employee extends Person { department: string }\n","tokens":[{"type":"T_CLASS","context":"normal","value":"class","line":1,"start":0,"end":5},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":1,"start":6,"end":12},{"type":"T_LCURLY","context":"normal","value":"{","line":1,"start":13,"end":14},{"type":"T_IDENTIFIER","context":"normal","value":"name","line":1,"start":15,"end":19},{"type":"T_COLON","context":"type","value":":","line":1,"start":19,"end":20},{"type":"T_STRING_TYPE","context":"type","value":"string","line":1,"start":21,"end":27},{"type":"T_RCURLY","context":"normal","value":"}","line":1,"start":28,"end":29},{"type":"T_CLASS","context":"normal","value":"class","line":2,"start":30,"end":35},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":2,"start":36,"end":44},{"type":"T_EXTENDS","context":"normal","value":"extends","line":2,"start":45,"end":52},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":2,"start":53,"end":59},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":60,"end":61},{"type":"T_IDENTIFIER","context":"normal","value":"department","line":2,"start":62,"end":72},{"type":"T_COLON","context":"type","value":":","line":2,"start":72,"end":73},{"type":"T_STRING_TYPE","context":"type","value":"string","line":2,"start":74,"end":80},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":81,"end":82}],"errors":[]}
It’s valid to use an Employee
instance where a Person
instance is expected.
1
2
3
4
5
6
|
class Person { name: string }
class Employee extends Person { department: string }
var employee: Employee = new Employee;
var person: Person = employee;
|
{"value":"// @flow\nclass Person { name: string }\nclass Employee extends Person { department: string }\n\nvar employee: Employee = new Employee;\nvar person: Person = employee; // OK\n","tokens":[{"type":"Line","context":"comment","value":"// @flow","line":1,"start":0,"end":8},{"type":"T_CLASS","context":"normal","value":"class","line":2,"start":9,"end":14},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":2,"start":15,"end":21},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"name","line":2,"start":24,"end":28},{"type":"T_COLON","context":"type","value":":","line":2,"start":28,"end":29},{"type":"T_STRING_TYPE","context":"type","value":"string","line":2,"start":30,"end":36},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":37,"end":38},{"type":"T_CLASS","context":"normal","value":"class","line":3,"start":39,"end":44},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":3,"start":45,"end":53},{"type":"T_EXTENDS","context":"normal","value":"extends","line":3,"start":54,"end":61},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":3,"start":62,"end":68},{"type":"T_LCURLY","context":"normal","value":"{","line":3,"start":69,"end":70},{"type":"T_IDENTIFIER","context":"normal","value":"department","line":3,"start":71,"end":81},{"type":"T_COLON","context":"type","value":":","line":3,"start":81,"end":82},{"type":"T_STRING_TYPE","context":"type","value":"string","line":3,"start":83,"end":89},{"type":"T_RCURLY","context":"normal","value":"}","line":3,"start":90,"end":91},{"type":"T_VAR","context":"normal","value":"var","line":5,"start":93,"end":96},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":5,"start":97,"end":105},{"type":"T_COLON","context":"type","value":":","line":5,"start":105,"end":106},{"type":"T_IDENTIFIER","context":"type","value":"Employee","line":5,"start":107,"end":115},{"type":"T_ASSIGN","context":"normal","value":"=","line":5,"start":116,"end":117},{"type":"T_NEW","context":"normal","value":"new","line":5,"start":118,"end":121},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":5,"start":122,"end":130},{"type":"T_SEMICOLON","context":"normal","value":";","line":5,"start":130,"end":131},{"type":"T_VAR","context":"normal","value":"var","line":6,"start":132,"end":135},{"type":"T_IDENTIFIER","context":"normal","value":"person","line":6,"start":136,"end":142},{"type":"T_COLON","context":"type","value":":","line":6,"start":142,"end":143},{"type":"T_IDENTIFIER","context":"type","value":"Person","line":6,"start":144,"end":150},{"type":"T_ASSIGN","context":"normal","value":"=","line":6,"start":151,"end":152},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":6,"start":153,"end":161},{"type":"T_SEMICOLON","context":"normal","value":";","line":6,"start":161,"end":162},{"type":"Line","context":"comment","value":"// OK","line":6,"start":163,"end":168}],"errors":[]}
However, it is not valid to use an object containing an Employee
instance
where an object containing a Person
instance is expected.
1
2
3
4
5
6
7
|
class Person { name: string }
class Employee extends Person { department: string }
var employee: { who: Employee } = { who: new Employee };
var person: { who: Person } = employee;
|
Cannot assign `employee` to `person` because `Employee` [1] is incompatible with `Person` [2] in property `who`. [incompatible-type]
{"value":"// @flow\nclass Person { name: string }\nclass Employee extends Person { department: string }\n\nvar employee: { who: Employee } = { who: new Employee };\n// $ExpectError\nvar person: { who: Person } = employee; // Error\n","tokens":[{"type":"Line","context":"comment","value":"// @flow","line":1,"start":0,"end":8},{"type":"T_CLASS","context":"normal","value":"class","line":2,"start":9,"end":14},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":2,"start":15,"end":21},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"name","line":2,"start":24,"end":28},{"type":"T_COLON","context":"type","value":":","line":2,"start":28,"end":29},{"type":"T_STRING_TYPE","context":"type","value":"string","line":2,"start":30,"end":36},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":37,"end":38},{"type":"T_CLASS","context":"normal","value":"class","line":3,"start":39,"end":44},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":3,"start":45,"end":53},{"type":"T_EXTENDS","context":"normal","value":"extends","line":3,"start":54,"end":61},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":3,"start":62,"end":68},{"type":"T_LCURLY","context":"normal","value":"{","line":3,"start":69,"end":70},{"type":"T_IDENTIFIER","context":"normal","value":"department","line":3,"start":71,"end":81},{"type":"T_COLON","context":"type","value":":","line":3,"start":81,"end":82},{"type":"T_STRING_TYPE","context":"type","value":"string","line":3,"start":83,"end":89},{"type":"T_RCURLY","context":"normal","value":"}","line":3,"start":90,"end":91},{"type":"T_VAR","context":"normal","value":"var","line":5,"start":93,"end":96},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":5,"start":97,"end":105},{"type":"T_COLON","context":"type","value":":","line":5,"start":105,"end":106},{"type":"T_LCURLY","context":"type","value":"{","line":5,"start":107,"end":108},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":5,"start":109,"end":112},{"type":"T_COLON","context":"type","value":":","line":5,"start":112,"end":113},{"type":"T_IDENTIFIER","context":"type","value":"Employee","line":5,"start":114,"end":122},{"type":"T_RCURLY","context":"type","value":"}","line":5,"start":123,"end":124},{"type":"T_ASSIGN","context":"normal","value":"=","line":5,"start":125,"end":126},{"type":"T_LCURLY","context":"normal","value":"{","line":5,"start":127,"end":128},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":5,"start":129,"end":132},{"type":"T_COLON","context":"normal","value":":","line":5,"start":132,"end":133},{"type":"T_NEW","context":"normal","value":"new","line":5,"start":134,"end":137},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":5,"start":138,"end":146},{"type":"T_RCURLY","context":"normal","value":"}","line":5,"start":147,"end":148},{"type":"T_SEMICOLON","context":"normal","value":";","line":5,"start":148,"end":149},{"type":"Line","context":"comment","value":"// $ExpectError","line":6,"start":150,"end":165},{"type":"T_VAR","context":"normal","value":"var","line":7,"start":166,"end":169},{"type":"T_IDENTIFIER","context":"normal","value":"person","line":7,"start":170,"end":176},{"type":"T_COLON","context":"type","value":":","line":7,"start":176,"end":177},{"type":"T_LCURLY","context":"type","value":"{","line":7,"start":178,"end":179},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":7,"start":180,"end":183},{"type":"T_COLON","context":"type","value":":","line":7,"start":183,"end":184},{"type":"T_IDENTIFIER","context":"type","value":"Person","line":7,"start":185,"end":191},{"type":"T_RCURLY","context":"type","value":"}","line":7,"start":192,"end":193},{"type":"T_ASSIGN","context":"normal","value":"=","line":7,"start":194,"end":195},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":7,"start":196,"end":204},{"type":"T_SEMICOLON","context":"normal","value":";","line":7,"start":204,"end":205},{"type":"Line","context":"comment","value":"// Error","line":7,"start":206,"end":214}],"errors":[{"id":"E1","messages":[{"id":"E1M1","description":"Cannot assign `employee` to `person` because `Employee` [1] is incompatible with `Person` [2] in property `who`. [incompatible-type]","context":"var person: { who: Person } = employee; // Error","source":"-","start":{"line":7,"column":31,"offset":196},"end":{"line":7,"column":38,"offset":204}}],"operation":null}]}
This is an error because objects are mutable. The value referenced by the
employee
variable is the same as the value referenced by the person
variable.
1
|
person.who = new Person;
|
{"value":"person.who = new Person;\n","tokens":[{"type":"T_IDENTIFIER","context":"normal","value":"person","line":1,"start":0,"end":6},{"type":"T_PERIOD","context":"normal","value":".","line":1,"start":6,"end":7},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":1,"start":7,"end":10},{"type":"T_ASSIGN","context":"normal","value":"=","line":1,"start":11,"end":12},{"type":"T_NEW","context":"normal","value":"new","line":1,"start":13,"end":16},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":1,"start":17,"end":23},{"type":"T_SEMICOLON","context":"normal","value":";","line":1,"start":23,"end":24}],"errors":[]}
If we write into the who
property of the person
object, we’ve also changed
the value of employee.who
, which is explicitly annotated to be an Employee
instance.
If we prevented any code from ever writing a new value to the object through
the person
variable, it would be safe to use the employee
variable. Flow
provides a syntax for this:
1
2
3
4
5
6
7
8
|
class Person { name: string }
class Employee extends Person { department: string }
var employee: { who: Employee } = { who: new Employee };
var person: { +who: Person } = employee;
person.who = new Person;
|
Cannot assign `new Person` to `person.who` because property `who` is not writable. [cannot-write]
{"value":"// @flow\nclass Person { name: string }\nclass Employee extends Person { department: string }\n\nvar employee: { who: Employee } = { who: new Employee };\nvar person: { +who: Person } = employee; // OK\n// $ExpectError\nperson.who = new Person; // Error!\n","tokens":[{"type":"Line","context":"comment","value":"// @flow","line":1,"start":0,"end":8},{"type":"T_CLASS","context":"normal","value":"class","line":2,"start":9,"end":14},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":2,"start":15,"end":21},{"type":"T_LCURLY","context":"normal","value":"{","line":2,"start":22,"end":23},{"type":"T_IDENTIFIER","context":"normal","value":"name","line":2,"start":24,"end":28},{"type":"T_COLON","context":"type","value":":","line":2,"start":28,"end":29},{"type":"T_STRING_TYPE","context":"type","value":"string","line":2,"start":30,"end":36},{"type":"T_RCURLY","context":"normal","value":"}","line":2,"start":37,"end":38},{"type":"T_CLASS","context":"normal","value":"class","line":3,"start":39,"end":44},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":3,"start":45,"end":53},{"type":"T_EXTENDS","context":"normal","value":"extends","line":3,"start":54,"end":61},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":3,"start":62,"end":68},{"type":"T_LCURLY","context":"normal","value":"{","line":3,"start":69,"end":70},{"type":"T_IDENTIFIER","context":"normal","value":"department","line":3,"start":71,"end":81},{"type":"T_COLON","context":"type","value":":","line":3,"start":81,"end":82},{"type":"T_STRING_TYPE","context":"type","value":"string","line":3,"start":83,"end":89},{"type":"T_RCURLY","context":"normal","value":"}","line":3,"start":90,"end":91},{"type":"T_VAR","context":"normal","value":"var","line":5,"start":93,"end":96},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":5,"start":97,"end":105},{"type":"T_COLON","context":"type","value":":","line":5,"start":105,"end":106},{"type":"T_LCURLY","context":"type","value":"{","line":5,"start":107,"end":108},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":5,"start":109,"end":112},{"type":"T_COLON","context":"type","value":":","line":5,"start":112,"end":113},{"type":"T_IDENTIFIER","context":"type","value":"Employee","line":5,"start":114,"end":122},{"type":"T_RCURLY","context":"type","value":"}","line":5,"start":123,"end":124},{"type":"T_ASSIGN","context":"normal","value":"=","line":5,"start":125,"end":126},{"type":"T_LCURLY","context":"normal","value":"{","line":5,"start":127,"end":128},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":5,"start":129,"end":132},{"type":"T_COLON","context":"normal","value":":","line":5,"start":132,"end":133},{"type":"T_NEW","context":"normal","value":"new","line":5,"start":134,"end":137},{"type":"T_IDENTIFIER","context":"normal","value":"Employee","line":5,"start":138,"end":146},{"type":"T_RCURLY","context":"normal","value":"}","line":5,"start":147,"end":148},{"type":"T_SEMICOLON","context":"normal","value":";","line":5,"start":148,"end":149},{"type":"T_VAR","context":"normal","value":"var","line":6,"start":150,"end":153},{"type":"T_IDENTIFIER","context":"normal","value":"person","line":6,"start":154,"end":160},{"type":"T_COLON","context":"type","value":":","line":6,"start":160,"end":161},{"type":"T_LCURLY","context":"type","value":"{","line":6,"start":162,"end":163},{"type":"T_PLUS","context":"type","value":"+","line":6,"start":164,"end":165},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":6,"start":165,"end":168},{"type":"T_COLON","context":"type","value":":","line":6,"start":168,"end":169},{"type":"T_IDENTIFIER","context":"type","value":"Person","line":6,"start":170,"end":176},{"type":"T_RCURLY","context":"type","value":"}","line":6,"start":177,"end":178},{"type":"T_ASSIGN","context":"normal","value":"=","line":6,"start":179,"end":180},{"type":"T_IDENTIFIER","context":"normal","value":"employee","line":6,"start":181,"end":189},{"type":"T_SEMICOLON","context":"normal","value":";","line":6,"start":189,"end":190},{"type":"Line","context":"comment","value":"// OK","line":6,"start":191,"end":196},{"type":"Line","context":"comment","value":"// $ExpectError","line":7,"start":197,"end":212},{"type":"T_IDENTIFIER","context":"normal","value":"person","line":8,"start":213,"end":219},{"type":"T_PERIOD","context":"normal","value":".","line":8,"start":219,"end":220},{"type":"T_IDENTIFIER","context":"normal","value":"who","line":8,"start":220,"end":223},{"type":"T_ASSIGN","context":"normal","value":"=","line":8,"start":224,"end":225},{"type":"T_NEW","context":"normal","value":"new","line":8,"start":226,"end":229},{"type":"T_IDENTIFIER","context":"normal","value":"Person","line":8,"start":230,"end":236},{"type":"T_SEMICOLON","context":"normal","value":";","line":8,"start":236,"end":237},{"type":"Line","context":"comment","value":"// Error!","line":8,"start":238,"end":247}],"errors":[{"id":"E1","messages":[{"id":"E1M1","description":"Cannot assign `new Person` to `person.who` because property `who` is not writable. [cannot-write]","context":"person.who = new Person; // Error!","source":"-","start":{"line":8,"column":8,"offset":220},"end":{"line":8,"column":10,"offset":223}}],"operation":null}]}
The plus sign indicates that the who
property is “covariant.” Using a covariant
property allows us to use objects which have subtype-compatible values for that
property. By default, object properties are invariant, which allow both reads
and writes, but are more restrictive in the values they accept.
Read more about property variance.