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