Why can't I assign a typescript generic type to a union of that type?

Adrian Bielefeldt

I have the following types in one of my webapps:

import React, {FC} from 'react';
import { Table, Tbody, Td, Th, Thead, Tr, chakra } from '@chakra-ui/react';
import { TriangleDownIcon, TriangleUpIcon } from '@chakra-ui/icons'
import {useTable, useSortBy, Column} from 'react-table'
import {Adult, Child} from "~/src/types/models";

type PersonTablePropData =
    | {
        persons: Adult[]
        columns: readonly Column<Adult>[]
      }
    | {
        persons: Child[]
        columns: readonly Column<Child>[]
      }

export type PersonTableProps = PersonTablePropData & {
    showPerson: (personId: number) => void
}

const PersonTable: FC<PersonTableProps> = ({ persons, showPerson, columns }) => {

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
    useTable<Adult|Child>({ columns, data: persons }, useSortBy)
...

This results in an error on ´columns´ in the last line:

Type 'readonly Column<Adult>[] | readonly Column<Child>[]' is not assignable to type 'readonly Column<Adult | Child>[]'.

This leaves me at a bit of a loss: Shouldn't readonly Column<Adult>[] be assiganble to readonly Column<Adult | Child>[]? As far as I can see, it would be a straight subset, so what is going wrong here?

captain-yossarian from Ukraine

This leaves me at a bit of a loss: Shouldn't readonly Column[] be assiganble to readonly Column<Adult | Child>[]? As far as I can see, it would be a straight subset, so what is going wrong here?

You are absolutely right. It should be assignable under normal circumstances.

type Adult = {
  tag: 'Adult',
  name: string,
  age: number
}

type Child = {
  tag: 'Child',
  school: number
}

let adult: Adult[] = []
let both: Array<Child | Adult> = []

both = adult // ok,
adult = both // error, as expected

However, this is not true with function arguments. Spoiler: contravariance.

let adultFn = (arg: Adult[]) => { }
let bothFn = (arg: Array<Child | Adult>) => { }

bothFn = adultFn // error
adultFn = bothFn // ok

adult is no more assignable to bothFn. The arraw of inheritance change its way in an opposite direction.

Let's make it more closer to your example:


type Adult = {
  tag: 'Adult',
  name: string,
  age: number
}

type Child = {
  tag: 'Child',
  school: number
}

type Options<T> = {
  columns: T[],
  handler: (arg: T) => void
}

let adult: Adult[] | Child[] = []

let hook = <T,>(arg: Options<T>) => { }

// same error as you have
hook<Adult | Child>({ columns: adult })

As you ,ight have noticed, this error is same as you have. Now, try to remove handler: (arg: T) => void from Options<T> type. Error will disapear. Why ? Because of contravariance. This is whats make your code more safer. useTable hook uses under the hood UseTableOptions type, where provided generic D is used in a contravarian position - in a position of argument.

type UseTableOptions<D extends object> = {
    columns: ReadonlyArray<Column<D>>;
    data: readonly D[];
} & Partial<{
    initialState: Partial<TableState<D>>;
    stateReducer: (newState: TableState<D>, action: ActionType, previousState: TableState<D>, instance?: TableInstance<D>) => TableState<D>;
    useControlledState: (state: TableState<D>, meta: Meta<D>) => TableState<D>;
    defaultColumn: Partial<Column<D>>;
    getSubRows: (originalRow: D, relativeIndex: number) => D[];
    getRowId: (originalRow: D, relativeIndex: number, parent?: Row<D>) => string;
    autoResetHiddenColumns: boolean;
}>

Please see another one simplified example:

type Options<T> = {
  handler: (arg: T) => void
}

type SuperType = string;
type SubType = 'hello'

declare let superType: SuperType;
declare let subType: SubType;

superType = subType // ok
subType = superType // error


let superTypeObj: Options<SuperType> = {
  handler: (arg) => { }
}


let subTypeObj: Options<SubType> = {
  handler: (arg) => { }
}

// opposite direction
superTypeObj = subTypeObj // error
subTypeObj = superTypeObj // ok

Playground

More about *-variance you can find here and here

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

Why can't I assign type's value to an interface implementing methods with receiver type pointer to that type?

Why can't I use 'as' with generic type parameter that is constrained to be an interface?

Can I assign a default type to generic type T in Swift?

Why can't I create an array of an inner class of a generic type?

Why can't I cast one instantiation of a generic type to another?

Why can't I use covariance with two generic type parameters?

Why can't TypeScript infer a generic type when it is in a nested object?

Assign to union type in Typescript

Could typescript narrow union type in generic type?

Why I can't assign nullable type to Any in Kotlin?

How can I specify that Typescript generic T[K] is number type?

Why doesn't Typescript error generic union type?

Typescript generic union type

Why can't I call a generic function with a union type parameter?

Why I can't use a Class type as a generic parameter in Kotlin?

Why can't I use generic Type as an argument in Dart 2?

Typescript union type generic parameter

Why can't I return the generic type T from this function?

Why can't assign I <? extends Type> to <Type>?

Why can't I assign Nothing to a variable of type Unit?

TS: why can assign not valid type to generic type variable?

Why can't I convert this type into a generic one?

Why can't I cast a more derived generic type to a less derived generic type?

TypeScript: Why can't I assign a valid field of an object with type { a: "a", b: "b" }

Why can't TypeScript infer this generic type?

Why can't I assign default to my notnull generic type?

Why cannot a generic TypeScript function with an argument of type from a type union infer the same type as the return type?

I can't assign value to a generic variable with same type

Why can I use `as` but not assign the type for mapped values in typescript?