Contrasting covariant and contravariant positions within Typescript

I'm currently diving into the examples provided in the Typescript advanced types handbook to broaden my understanding.

According to the explanation:

The next example showcases how having multiple potential values for the same type variable in co-variant positions leads to an inferred union type:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

Similarly, when there are multiple possible values for the same type variable in contra-variant positions, it results in an inferred intersection type:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

This raises the question: why are the object properties in the first example referred to as "co-variant positions" while the function arguments in the second example are called "contra-variant positions"?

Additionally, the second example appears to result in never and I am unsure if any additional configuration might be needed to make it work properly.

Answer №1

Your keen observation about one of the examples resulting in never is accurate, and you are not overlooking any compiler settings. In more recent TypeScript versions, intersections of primitive types lead to a resolution of never. If you switch back to an older version, you will still observe string & number. The newer version showcases contravariant position behavior when using object types:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T21 = Bar<{ a: (x: { h: string }) => void, b: (x: { g: number }) => void }>;  // {h: string; } & { g: number;}

Playground Link

The reason why function parameters are contravariant while properties are covariant comes down to a tradeoff between type safety and usability.

Function arguments being contravariant makes sense because you can only safely call a function with a subtype of the argument, not a base type.

class Animal { eat() { } }
class Dog extends Animal { wof() { } }

type Fn<T> = (p: T) => void
var animalFn: Fn<Animal> = a => a.eat();
var dogFn: Fn<Dog> = d => { d.eat(); d.wof() }
dogFn(new Animal()) // error, invoking d.wof would fail 
animalFn = dogFn; // thus, this also fails

animalFn(new Dog()) // This is okay
dogFn = animalFn; // this is also okay 

Playground Link

As for why properties are covariant, the explanation is slightly more complex. Essentially, a field position like { a: T } would result in an invariant type, but that approach could be challenging. Therefore, in TypeScript, by definition, a field type position (as seen in T) renders the type covariant in that field type (thus, { a: T } is covariant in T). While we could demonstrate how a being read-only results in covariance and write-only leads to contravariance, combining both scenarios gives us invariance. However, I'll leave you with an example where default covariant behavior can cause runtime errors even in correctly typed code:

type SomeType<T> = { a: T }

function foo(a: SomeType<{ foo: string }>) {
    a.a = { foo: "" } // no bar here, not needed
}
let b: SomeType<{ foo: string, bar: number }> = {
    a: { foo: "", bar: 1 }
}

foo(b) // valid since T is in a covariant position, making SomeType<{ foo: string, bar: number }> assignable to SomeType<{ foo: string }>
b.a.bar.toExponential() // Runtime error as nobody in foo assigned bar

Playground Link

You may also find my post on variance in TypeScript interesting by clicking here.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Managing event date changes in Angular PrimeNG FullCalendar

Is there a way to capture an event when the date of an event is changed? I would like to receive the new date in a function. Is this functionality possible? For example, if I have an event scheduled for 2020-01-01 and I drag it to date 2020-01-10, how can ...

In Typescript, we can streamline this code by assigning a default value of `true` to `this.active` if `data.active

I am curious if there is a better way to write the statement mentioned in the title. Could it be improved with this.active = data.active || true? ...

TS2604: The JSX element '...' lacks any construct or call signatures and is unable to be processed

As part of our company's initiative to streamline development, I am working on creating a package that includes common components used across all projects. We primarily work with TypeScript, and I have successfully moved the code to a new project that ...

The challenge with the Optional Chaining operator in Typescript 3.7@beta

When attempting to utilize the Typescript optional chaining operator, I encountered the following exception: index.ts:6:1 - error TS2779: The left-hand side of an assignment expression may not be an optional property access. Here is my sample code: const ...

Adding a second interface to a Prop in Typescript React: a step-by-step guide

import { ReactNode, DetailedHTMLProps, FormHTMLAttributes } from "react"; import { FieldValues, SubmitHandler, useForm, UseFormReturn, } from "react-hook-form"; // I am looking to incorporate the DetailedHTMLProps<FormHTMLAt ...

Caution: The attribute name `data-*` is not recognized as valid

I am attempting to import an SVG file in my NEXT.js project using the babel-plugin-inline-react-svg. I have followed all the instructions and everything is functioning perfectly. // .babelrc { "presets": ["next/babel"], "plugin ...

What is the best approach for testing the TypeScript code below?

Testing the following code has been requested, although I am not familiar with it. import AWS from 'aws-sdk'; import db from './db'; async function uploadUserInfo(userID: number) { const user = db.findByPk(userID); if(!user) throw ...

Troubleshooting: Angular 6 Renderer2 Issue with Generating Dynamic DOM Elements for SELECT-Option

Currently, I am attempting to dynamically create a select option using Renderer2. Unfortunately, I am facing difficulties in creating the <Select></Select> element, but I can confirm that the <options> are being successfully created. Due ...

Do I have to wait for the HTTP get request to access the fetched object property?

I am currently working with Angular and TypeScript on a dish-detail component that is accessed through 'dishes/:id' The dish object returned has a property called components, which contains an array of objects with two properties: id: type stri ...

Using the `window` object in Jasmine with Angular, a mock can be created for testing purposes

In my current project, I have a function that I need to write unit tests for. Within this function, I am comparing the global objects window and parent using const isEqual = (window === parent). I am wondering what would be the most effective way to mock ...

The module cannot be located: Unable to find '../typings' in '/vercel/path0/pages'

Having trouble deploying my Next.js website through Vercel. It seems to be stuck at this stage. Can someone assist me, please? I've attempted deleting the node_modules folder and package-lock.json, then running npm install again, but unfortunately it ...

What is the process for initiating an Angular 2 Materialize component?

I'm new to using angular2 materialize and I've found that the CSS components work perfectly fine. However, I'm facing an issue when it comes to initializing components like 'select'. I'm unsure of how or where to do this initi ...

Troubleshooting the issue with the HTTPClient get method error resolution

Currently, I am attempting to capture errors in the HTTP get request within my service file. Below is the code snippet: import { Injectable } from '@angular/core'; import { PortfolioEpicModel, PortfolioUpdateStatus } from '../models/portfol ...

What is the best way to customize a button component's className when importing it into another component?

Looking to customize a button based on the specific page it's imported on? Let's dive into my button component code: import React from "react"; import "./Button.css"; export interface Props { // List of props here } // Button component def ...

Upgrade Angular from 12 to the latest version 13

I recently attempted to upgrade my Angular project from version 12 to 13 Following the recommendations provided in this link, which outlines the official Angular update process, I made sure to make all the necessary changes. List of dependencies for my p ...

Struggling to successfully pass a function as an argument to the setTimeout method within an array in node.js using TypeScript

Here is an example that successfully demonstrates a function being called using setTimeout: function displayMessage(msg: string){ console.log(msg); } setTimeout(displayMessage, 1000, ["Hi!"]; After one second, it will print out "Hi!" to the console. ...

How can you define the types of function arguments when destructuring arguments in TypeScript?

TS throws an error that states: Error:(8, 20) TS7031: Binding element 'on' implicitly has an 'any' type. Error:(8, 24) TS7031: Binding element 'children' implicitly has an 'any' type. Below is the function I am wor ...

Exploring the benefits of using getServerSideProps with NextJS and Typescript for

Dear community, I am facing a challenge with integrating NextJS Layout and Typescript. Although everything seems to be working fine, I can't seem to get rid of the squiggly line under the props when passing them from getServerSideProps. The prop {som ...

Choosing the initial choice with ngFor: A step-by-step guide

I'm having trouble selecting the first option after the user enters their email, but it remains unselected. Any ideas on how to solve this? view image here HTML Code: <label for="login"><b>User:</b></label> <inpu ...

A guide to implementing vue-i18n in Vue class components

Take a look at this code snippet: import Vue from 'vue' import Component from 'vue-class-component' @Component export default class SomeComponent extends Vue { public someText = this.$t('some.key') } An error is being thr ...