Home > Mobile >  Typescript generics: How to implicitly derive the type of a property of an object from another prope
Typescript generics: How to implicitly derive the type of a property of an object from another prope

Time:07-21

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.

CodePudding user response:

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
    }
]

CodePudding user response:

(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

  • Related