Last active
August 30, 2022 15:52
-
-
Save gvergnaud/57ef66ea05c539abb2eb787ea0433666 to your computer and use it in GitHub Desktop.
TypeScript type variance (covariance, contravariance and invariance)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* We call "variance" the direction of assignability between two types | |
* (in other words, which type is a subtype of the other). | |
* | |
* The same type can have a different variance in function of his position | |
* in a larger type, and this is what makes assignability in TypeScript | |
* not always straightforwards. | |
* | |
* There are 4 kinds of variance: | |
* - Covariant: if the type `a` is assignable to `a | b`. | |
* - Contravariant: if the type `a | b` is assignable to `a`. | |
* - Invariant: if types `a | b` and `a` aren't assignable in any direction. | |
* - Bivariant: if types `a | b` and `a` are assignable in both directions. | |
* | |
*/ | |
type AnyColor = 'red' | 'green' | 'blue' | |
type ToplistColor = 'red' | 'green' | |
type TableColor = 'green' | 'blue' | |
// Covariance is the case we are most familiar with. | |
// `let` and `const` are covariant positions, as well as function return types | |
// you can assign a ToplistColor to a AnyColor | |
const toplistColor: ToplistColor = 'red' | |
const color: AnyColor = toplistColor | |
// but not the opposite. | |
const color2: AnyColor = 'blue' | |
const toplistColor2: ToplistColor = color2 // ⚠️ doesn't typecheck | |
// In TypeScript, function arguments are contravariant positions, | |
// so the assignability of these types is "reversed". | |
// It means we can't assign a `(ToplistColor) => unknown` to a `(AnyColor) => unknown`: | |
const onToplistColorChange: (Color: ToplistColor) => void = () => {} | |
const onColorChange: (Color: AnyColor) => void = onToplistColorChange // ⚠️ doesn't typecheck | |
// It makes sense, because if you could assign `onToplistColorChange` | |
// to the type `(Color: AnyColor) => void`, you would be able to pass | |
// 'blue' to this function, even though it doesn't actually supports it. | |
// The opposite assignment works, however: | |
const onToplistColorChange2: (Color: AnyColor) => void = () => {} | |
const onColorChange2: (Color: ToplistColor) => void = onToplistColorChange2 | |
// That's because you can pass `'red' |'green'` to a function accepting | |
// `'red' | 'green' | 'blue'`. | |
// If you put a type of some variance in a data structure (object types or array types for instance) | |
// the variance "propagates" to the the type of the outer data structure: | |
const onToplistColorChangeProps: {onChange: (Color: ToplistColor) => void} = {onChange: () => {}} | |
const onColorChangeProps: {onChange: (Color: AnyColor) => void} = onToplistColorChangeProps // ⚠️ doesn't typecheck | |
// Which means that if you put a type in both covariant and contravariant position, | |
// your datastructure type becomes invariant, and you can no longer use | |
// subtyping: | |
// This will be invariant because | |
type ColorEditorProps<P extends AnyColor> = { | |
value?: P // this is covariant | |
onChange?: (v: P) => void // this is contravariant | |
} | |
// Since the object is invariant, it's not assignable in any way: | |
const toplistColorEditorProps: ColorEditorProps<ToplistColor> = {} | |
const colorEditorProps: ColorEditorProps<AnyColor> = toplistColorEditorProps // ⚠️ doesn't typecheck | |
const colorEditorProps2: ColorEditorProps<AnyColor> = {} | |
const toplistColorEditorProps2: ColorEditorProps<ToplistColor> = colorEditorProps2 // ⚠️ doesn't typecheck | |
// I'm not aware of any bivariant position in TS but, there may be some. `any` | |
// kinds of make all other types bivariant... | |
// Check out this TS playground for an interactive version of this: | |
// https://www.typescriptlang.org/play?#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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment