Search code examples
javascripttypescripttypeerrortypescript-generics

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


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?


Solution

  • 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