(I'm going to focus on your Column
example and ignore Foo
, because the inference issues are different, and you care about Column
more than Foo
).
Conceptually something like "a collection of objects, each of which is of type Column<D, V>
for a particular D
I know, but for some V
I don't care about" requires existentially quantified generic types. (I presume you need to know D
, since, if you don't, there's no way to call accessorFunction
).
But few languages support such types, and neither does TypeScript; see microsoft/TypeScript#14466 for the relevant feature request. If we did have them, you could say something like Array<Column<D, exists V>>
. But we don't, so we can't.
There are ways to encode existential types. The general method to use a Promise
-like data structure to invert control flow with generic callbacks. A generic function allows the caller to specify the type parameter while the implementer only knows that it's some type. It looks like this:
type SomeColumn<D> = <R>(cb: <V>(col: Column<D, V>) => R) => R;
A SomeColumn<D>
is like a Promise<Column<D, ??>>
's then()
method. It accepts a callback and then presumably calls the callback on the underlying Column<D, V>
it's holding. To turn a Column<D, V>
into a SomeColumn<D>
, we can use a helper function:
const someColumn = <D, V>(col: Column<D, V>): SomeColumn<D> => cb => cb(col);
Then your array could look like this:
const columns = [
someColumn({
accessorFunction: (a: string) => "test",
cell: (info) => info.toUpperCase()
}),
someColumn({
accessorFunction: (a: string) => a.length,
cell: info => info.toFixed(2)
})
];
That columns
is of type SomeColumn<string>
. If we want to process the array, we have to add that one nested callback. Say, like this:
const results = columns.map(
sc => sc(
col => col.cell(col.accessorFunction("hello"))
)
)
// const results: any[]
console.log(results); // ["TEST", "5.00"]
Note that I'm doing just about the only useful thing I can do with a Column<string, V>
where I don't know V
... that is, calling col.cell(col.accessorFunction(someString))
. Anyway, results
is an array of cell()
outputs (which you've typed as any
, so we have any[]
).
That might be overkill for your use case. Maybe all you care about is inference of the cell()
method's input, and you don't mind if columns
is an array like Array<Column<string,string> | Column<string, number>>
. If so, you can keep the helper function but just strip out any Promise
-like behavior out of it. It accepts a Column<D, V>
and returns a Column<D, V>
:
const column = <D, V>(col: Column<D, V>) => col;
const columns = [
column({
accessorFunction: (a: string) => "test",
cell: (info) => info.toUpperCase()
}),
column({
accessorFunction: (a: string) => a.length,
cell: info => info.toFixed(2)
})
]; // okay
But you will find it difficult to process these programmatically:
const results = columns.map(
col => col.cell(col.accessorFunction("hello")) // error!
// -----------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// compiler doesn't know that this will always be the right type for col.cell
);
There are workarounds there too, but they're annoying and you might as well just use a type assertion if you're careful to do so correctly:
const results = (columns as Array<Column<string, any>>).map(
col => col.cell(col.accessorFunction("hello"))
);
Playground link to code
Column<D, V>
case, inference is harder. I'd probably just use a single helper function as shown here. I could write up this version if you care aboutColumn<D, V>
more thanFoo<T>
. Let me know how to proceed. – GoodwillColumn<D, V>
function as an answer. – Nunley