Typescript generics: How to implicitly derive the type of a property of an object from another property of that object?
Asked Answered
N

2

5

I want to type objects within an array, so that one property of the object (here foo) implicitly defines the type for another property (bar) for each object seperately.

Is that possible?

type Foo<T> = {
  foo: T;
  bar: T;
};

const foos: Foo<any>[] = [ // <-- What to put here instead of "any"?
  { foo: "text", bar: "this is ok"}, 
  { foo: 666, bar: "this is NOT ok" }, // <-- should show error (must be of type number)
];

In the example foo and bar have the same type, in my real case scenario I have:

type Column<TData, TValue> = {
  accessorFunction: (data:TData) => TValue,
  cell: (info:TValue) => any
}

const columns = [
  {
    accessorFunction: ()=> "test",
    cell: (info) => info   // <-- info should be of type string
]

But I guess this has the same typing problem.

Nunley answered 19/7, 2022 at 10:55 Comment(4)
The term you are looking for is "existential types" and they are not directly supported in typescript. There are a few different approachs to somehow encoding them in TS. Here's oneCienfuegos
Without existential types, you need some kind of workaround. One way to proceed with arrays is to use a mapped tuple type and a helper function, like this. Another is to emulate existential types like this, but that's probably overkill. Do either of those meet your needs? If so I can write up an answer; if not, what am I missing?Goodwill
Note that for your 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 about Column<D, V> more than Foo<T>. Let me know how to proceed.Goodwill
@jcalz: Thank you very much. I'd be glad to accept your Column<D, V> function as an answer.Nunley
G
5

(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

Goodwill answered 20/7, 2022 at 20:2 Comment(0)
A
3

This throws the intended error:

type Foo<T> = {
    foo: T;
    bar: T;
}

const foos: (Foo<string> | Foo<number> | Foo<boolean>)[] = [ // <-- What to put here instead of "any"?
    { foo: "text", bar: "this is ok" },
    { foo: 666, bar: 2323 }, // <-- should show error (must be of type number)
]

Unfortunately, this is not so easy to apply to your second example.
However, it is possible to force the manual specification of the type by enable "noImplicitAny" in the tsconfig.json:

type Column<TData, TValue> = {
    accessorFunction: (data: TData) => TValue,
    cell: (info: TValue) => any
}

const column: (Column<any, string> | Column<any, number> | Column<any, boolean>)[] = [
    {
        accessorFunction: () => "test",
        cell: (info) => info  // <-- if you enable "noImplicitAny" you get an error here because you need to define the type
    }
]
Anarch answered 19/7, 2022 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.