Shinigami
Shinigami

Reputation: 685

Infer values of specific key in object-array

I want to write a DataTable class that is very smart

export interface ColumnDefinition<?> {
  readonly name: ?;
  title: string;
  align?: 'left' | 'center' | 'right';
  sortable?: boolean;
  hideable?: boolean;
  // ...
}

export interface DataTableOptions<?> {
  readonly columnDefinitions: ColumnDefinition<?>[];
  // ...
}

export class DataTable<?> {
  private readonly columnDefinitions: ReadonlyArray<ColumnDefinition>;

  constructor(options: DataTableOptions<?>) {
    this.columnDefinitions = options.columnDefinitions;
    // ...
  }

  // ...

  public title(columnName: ?): string {
    return this.columnDefinitions.find(({ name }) => name === columnName)?.title ?? '';
  }

  // ...
}

I put some ? in places where I don't know how to provide generic types

The goal is to call the following

const table = new DataTable({
  columnDefinitions: [
    { name: 'id', title: 'ID' },
    { name: 'v1', title: 'Value 1' },
    { name: 'v2', title: 'Value 2' }
  ],
  // ...
});

Then the table should be typed as follows: DataTable<'id' | 'v1' | 'v2'>
If someone tries to use multiple column definitions with the same name, an error occur

In addition, some member functions should benefit from it

table.title('id'); // 'ID'
table.title('test'); // compile time error

Upvotes: 2

Views: 106

Answers (2)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249646

Ensuring that we capture the actual names passed in can be easily done. We will capture the column definitions as whole in a tuple type (This will also help us when we try to test for uniqueness)

To capture the type of columnDefinitions as a tuple type, we need a type parameter, lets call it C with a constraint of [ColumnDefinition] | ColumnDefinition[]. The first [ColumnDefinition] ensures we get a tuple type, while the ColumnDefinition[] ensures we allow a tuple of any type.

To capture the name of each property we need to do a bit more. Firstly ColumnDefinition needs a type parameter extending string (interface ColumnDefinition<N extends string>{...}). This would allow us to write something like ColumnDefinition<'id'>

Having the possibility to keep the string literal type for the name in ColumnDefinition we need to go back to the constraint of C. We now need to specify a type parameter in the constraint.[ColumnDefinition<string>] | ColumnDefinition<string>[] would be valid but it will not capture string literal types, it will just infer string for all the items in the tuple. To get string literal types we would need an extra type parameter constrained to string (This will trigger the compiler to keep string literal types.

So the final definition of DataTable would be class DataTable<C extends [ColumnDefinition<V>] | ColumnDefinition<V>[], V extends string> {... }

With the C type in hand we can type the parameter to title relative to C. So we could write title(columnName: C[number]['name']): string

The uniqueness part is a bit more difficult to guarantee. We will need a recursive conditional type (which have all sorts of warnings attached to them). But it can be done. Below the IsUnique type will return {} if there are no duplicates or a tuple containing a custom error message which will cause an error when invoking the constructor.

The resulting solution:

export interface ColumnDefinition<N extends string> {
    readonly name: N;
    title: string;
    align?: 'left' | 'center' | 'right';
    sortable?: boolean;
    hideable?: boolean;
    // ...
}

export interface DataTableOptions<C extends ColumnDefinition<string>[]> {
    readonly columnDefinitions: C;
    // ...
}

type ColumnName<T> = T extends ColumnDefinition<infer N> ? N : never;
type IsUnique<T extends any[], E = never> = {
    next: ((...a: T) => void) extends ((h: infer H, ...t: infer R) => void) ?
        [ColumnName<H>] extends [E] ? ["Names are not unique", ColumnName<H>, "was found twice"] :
        IsUnique<R, E | ColumnName<H>>: ["NO", T]
    stop: {}
}[T extends [] ? "stop" : "next"];


export class DataTable<C extends [ColumnDefinition<V>] | ColumnDefinition<V>[], V extends string> {
    private readonly columnDefinitions: Readonly<C>;

    constructor(options: DataTableOptions<C>  & IsUnique<C> ) {
        this.columnDefinitions = options.columnDefinitions;
        // ...
    }

    public title(columnName: C[number]['name']): string {
        return this.columnDefinitions.find(({ name }) => name === columnName)?.title ?? '';
    }

    // ...
}

const table = new DataTable({
    columnDefinitions: [
        { name: 'id', title: 'ID' },
        { name: 'v1', title: 'Value 1', align: "right" },
        { name: 'v2', title: 'Value 2', align: "right" }
        // { name: 'id', title: 'ID' }, 
        // Comment the line above to get the error below
        // Type '{ columnDefinitions: [{ name: "id"; title: string; }, { name: "v1"; title: string; }, { name: "v2"; title: string; }, { name: "id"; title: string; }]; }' is not assignable to type '["Names are not unique", "id", "was found twice"]'.(2345)
    ],
    // ...
});
table.title("Id") // err
table.title("id") // ok


Playground Link

Upvotes: 2

Kirill Morozov
Kirill Morozov

Reputation: 603

I believe type variable generics is what you are looking for! Simple example of the class that operates on the generic type variable would be:

interface Data<T> {
    val: T;
}

class DataTable<T> {
    private data: Data<T>;

    constructor(val: T) {
        this.data = {
            val,
        }
    }
}

ts playground

You have updated your question, and i am not sure what exactly you are doing anymore 100%, but i think that you want to use type alias, and extend it with type variable like so:

type Id = "id";

type IdObject = {
    id: Id,
    title: string
}

type V1 = "v1";

type V1Object = {
    name: V1;
    title: string;
}

type EitherOne = V1Object | IdObject;

interface Data<T extends EitherOne> {
    val: T;
}

class DataTable<T> {
    private data: Data<T>;

    constructor(val: T) {
        this.data = {
            val,
        }
    }
}

ts playground

Upvotes: 1

Related Questions