TS Meetup #5.5 - Tițian Cericova-Dragomir - Understanding Variance in the TypeScript Type System

Prisma · 3K views · 2021 · 43:02

Open on YouTube

Programming language designWeb developmenttalk

Why Stack Overflow recommends it

Stack Overflow users link it when answering TypeScript questions about assignability errors, method versus function parameter bivariance, and invariant generic types.

Recommended by 2 people across 5 different months, 2021 to 2025.

What it is

A 43-minute TypeScript Berlin meetup talk by Titian Cernicova-Dragomir, a Bloomberg engineer and TypeScript compiler contributor. It explains variance (covariance, contravariance, bivariance, invariance) and how it drives assignability across the TypeScript type system, starting from why Array<Cat> is assignable to Array<Animal>.

What people said

Recommends Titian Cernicova-Dragomir · 2021-11 · Stack Overflow · on “Ignoring generic type argument when the specifity is not needed in Typescript”

TL;DR: There is no ? syntax in typescript. It really depends on the variance of your generic type if you can use anything except any , but you probably will use any classes usually end up being invariant. At it's core this is a question of variance. If you want to know more on that you can check out my talk here [this video] If the class is co-varinat you can use ElaborateStuff and any instantiation of SomeStuff will be compatible with it as SomeStuff<ElaborateStuff> will be the base class of all instantiations: type ElaborateStuff = { a: string } class SomeStuff<T extends ElaborateStuff> { getVStuff(): T { return null!; } } function doSimpleThingsElaborateStuff(stuff: SomeStuff<ElaborateStuff>):

Recommends Titian Cernicova-Dragomir · 2021-12 · Stack Overflow · on “Why does this arrow function in an interface not compile?”

@Liam Since jcalz beat me with a much more detailed answer :), I'll just link my talk on variance here maybe you'll find it useful [this video]

Recommends Titian Cernicova-Dragomir · 2021-12 · Stack Overflow · on “Why does typescript disregard strict null checks when implementing abstract functions?”

Method parameter types are checked bivariantly, so as long as the parameters types of the implementation and the abstract signature are related in either direction it will type check (in this case string is a sub type of string | null ) You could use function signature syntax to avoid this but only for interfaces (derived classes can't implement function members as methods unfortunately ex [LINK] ): export interface Foo { bar: (param: string | null) => string } export class ConcreteFoo implements Foo{ bar(param: string /* | null */): string { // error return param! } } Playground Link [LINK] There is currently no flag to change this behavior (as of TS 4.6). You can read a little bit about th

Recommends Titian Cernicova-Dragomir · 2022-02 · Stack Overflow · on “Why does TypeScript allow implementation of method expecting number or string to only accept number?”

Unfortunately this is one of the loopholes in type safety caused by the fact that method parameters are checked bi-variantly. This means that as long as there is a relationship between the function parameters it doesn't matter in which direction that relationship is. Here's a link to the related TypeScript handbook section: Function Parameter Bivariance [LINK] The reasoning for this is explained in the PR [LINK] that introduces strict function types (i.e. contravariant parameter types for function signatures) and is basically that if methods were checked contravariantly it would result in most generic types being invariant (so you couldn't assign Array<Cat> to Array<Animal> ). One solution i

Recommends Titian Cernicova-Dragomir · 2022-03 · Stack Overflow · on “Difference between functionName:()=>void; and functionName():void; in an interface”

One is a field that happens to be a function, the other is a method. This might not seem like a big deal at first (for interfaces), and in most cases it is not. There is however a subtle difference between the two, namely variance. Parameters for function signatures are contra-variant, while for methods parameters they are bi-varaint. This comes into play when checking compatibility between types and when implementing the interface. Now in your case since you don't actually have any parameters, this has no impact, but it you were to have a parameter, this can become importat: { interface Human { greet: (p: string | null) => void; } class HumanImpl implements Human { greet(p: string) { // Err

Recommends Alexander Nenashev · 2025-05 · Stack Overflow · on “TypeScript allows passing Readonly<T> to a mutating function”

Parameter variance in TS is a kind of mess. A good video about variance: [this video] . Seems in the case the parameter is bivariant. I would try to incorporate "mutable" branded type, that way you could force using Readonly (it's convoluted but makes some sense): Playground [LINK] { type F<T> = (t: Readonly<T>) => void; type Params = Mutable<{ a: number }>; type Mutable<T> = T & {__mutable: never}; type Readonly<T> = Omit<{ readonly [P in keyof T]: T[P]; }, '__mutable'>; // a utility to easily create mutable values; const mutable = <T,>(a: T) => a as Mutable<T>; const params = {a: 1}; mutate(params); // ERROR mutate2(params); // OK const params2 = mutable({a: 1}); mutate(params2); // OK mutate2(

Cites millsp · 2021-07 · Stack Overflow · on “Typescript linked generics not properly discriminated”

This happens because of variance. TypeScript is preventing you from doing a mistake. Look at the following example: // you can't assign a function with a broader result to a narrower one declare let fnA: () => 1 | 2 declare let fnB: () => 1 fnB = fnA // you can't assign a function with narrower params to a broader one declare let fnC: (a: 1 | 2) => 3 declare let fnD: (a: 1) => 3 fnC = fnD In the last example, because you are assigning fnD to fnC , you are meaning that fnD can handle the same input as fnC - but that's not true. This could lead to a runtime error. When you see such error messages, remember that TypeScript is just warning you that you are not complying with its variance rules.

Cites Titian Cernicova-Dragomir · 2021-12 · Stack Overflow · on “How to make interface which has a function that accepts its own type and works with interface implementations”

I can think of two ways to solve this. Both with some drawbacks. The first would be to use polymorphic this in the interface. This means the type of the parameter will be whatever the implementer is: interface I { test: (a: this) => boolean; } class Test implements I { constructor (public additional: number) {} test (a: Test) { return false; } } let o:I = new Test(1); // error Playground Link [LINK] The problem with this approach is you lose assignability to the interface. This might be fine depending on your use case. The other option is to use a method signature in your interface instead of a function signature. Method signatures are checked bivariantly (I have a video here [this video] if you

Cites Titian Cernicova-Dragomir · 2022-03 · Stack Overflow · on “Type U (generic) is not assignable to type unknown”

Your type is probably invariant on the type parameter T . This happens if you use a type parameter in both a covariant position (such as a field) and a contravariant position (such a as a function parameter). You can learn more about variance here [this video] export class Repository<T> { private readonly collection: Record<string, T> = {}; private readonly subrepositories: Record<string, Repository<unknown>> = {}; fn!: (p: T) => void; // Remove this and asssignability works subrepository<U>(docName: string) { if (!this.subrepositories[docName]) { this.subrepositories[docName] = new Repository<U>(); } return this.subrepositories[docName]; } } Playground Link [LINK] The reason this doesn't work is

Cites Titian Cernicova-Dragomir · 2023-01 · Stack Overflow · on “How to derive types from the passed arguments?”

You can't do this 100% type safe. The way UseFormReturn it is invariant (so there is no inheritance relationship between UseFormReturn<CustomFieldValues> and UseFormReturn<FormParams> , see my talk on variance here [this video] ) You can however use to more relaxed checking of assignability when it comes to overloads. So we will have a public function signature that allows us to pass in the derived type and an implementation signature that will use UseFormReturn<CustomFieldValues> . Now I want to stress this will not be 100% type safe, you might be able to do things that you should be allowed, but this is as good it's going to get function useCustomHook<T extends CustomFieldValues>(args: HookPara