Skip to content

Instantly share code, notes, and snippets.

@gvergnaud
Last active August 30, 2022 15:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gvergnaud/57ef66ea05c539abb2eb787ea0433666 to your computer and use it in GitHub Desktop.
Save gvergnaud/57ef66ea05c539abb2eb787ea0433666 to your computer and use it in GitHub Desktop.
TypeScript type variance (covariance, contravariance and invariance)
/**
* 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