2

Using TypeScript, I want to create an Array of different generics.

My goal of my generic class is to represent table columns, and the T holds the type of the value the column will hold. This looks as follows:

export class TableColumnConfig<T> { ... }

When I want to put multiple columns for a table into the array, I do this:

const columns: TableColumnConfig<any>[] = [];

Then I would do something like this:

columns.push(new TableColumnConfig<number>());
columns.push(new TableColumnConfig<string>());

This works, but the thing is that eslint tells me that I should not use any

Unexpected any. Specify a different type.eslint@typescript-eslint/no-explicit-any

Eslint suggest to use unknown.

So I do this:

const columns: TableColumnConfig<unknown>[] = [];

So of course, I get the following error:

Argument of type 'TableColumnConfig<number>' is not assignable to parameter of type 'TableColumnConfig<unknown>'. Type 'unknown' is not assignable to type 'number'.ts(2345)

Is there any way of satisfying eslint as well as getting no typescript syntax error?

Is it fine in this case to ignore the warning of eslint?

Some advice is much appreciated! :)

EDIT: This is how my class and the builder that I use for it looks like:

export class TableColumnConfig<T> {

    // lots of properties (that don't use T)

    format?: (val: T) => string;
    sort?: (val1: T, val2: T) => number;

    constructor() {
        // ...
    }
}

export class TableColumnConfigBuilder<T> implements ITableColumnConfigBuilder<T> {
  private tableColumnConfig: TableColumnConfig<T>;

  constructor() /* some mandatory properties */
  {
    this.tableColumnConfig = new TableColumnConfig(
      sourceAddress,
      parameterAddress,
      dataType,
      mainLabel
    );
  }

  // ...

  setFormat(format: (val: T) => string): this {
    this.tableColumnConfig.format = format;
    return this;
  }
  setSort(sort: (val1: T, val2: T) => number): this {
    this.tableColumnConfig.sort = sort;
    return this;
  }

  get(): TableColumnConfig<T> {
    return this.tableColumnConfig;
  }
}


interface ITableColumnConfigBuilder<T> {
    // ...
  setFormat(format: (val: T) => string): this;
  setSort(sort: (val1: T, val2: T) => number): this;
}
Walnussbär
  • 567
  • 5
  • 19
  • 1
    I can't reproduce the error: https://tsplay.dev/N9nVMm. Can you share a minimum reproducible example? – Tobias S. Oct 21 '22 at 09:28
  • @TobiasS. - And here I just took the error at face value. :-) Oops. I think my answer still applies, but I should have double-checked what the question said. – T.J. Crowder Oct 21 '22 at 09:30

2 Answers2

1

You can make the type of columns precisely reflect the types of the columns it contains:

const columns: (TableColumnConfig<number> | TableColumnConfig<string>)[] = [];
// −−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Playground link

Note that TypeScript will do that for you if you define columns with a literal and don't provide an explicit type:

const columns = [
    new TableColumnConfig<number>(),
    new TableColumnConfig<string>(),
];

Playground link

Of course, that's not always possible.


But if you don't want to be precise about the type, then any is where you'd go.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I don't understand why this works in the playground. As soon as I replace any with unknown, I get the above mentioned error ("Argument of type 'TableColumnConfig' is not assignable ...") – Walnussbär Oct 21 '22 at 09:37
  • 1
    @Walnussbär - please share the definition of `TableColumnConfig`. The problem is likely in there. – Tobias S. Oct 21 '22 at 09:38
  • 1
    @TobiasS. I edited my initial post with the source code of the `TableColumnConfig` class – Walnussbär Oct 21 '22 at 09:52
  • @Walnussbär - Thanks. I've updated the answer. Fundamentally, either be precise about the type, or use `any`. – T.J. Crowder Oct 21 '22 at 09:57
1

The error you are seeing here is related to variance. The two functions format and sort use T in a contravariant position making the type TableColumnConfig contravariant which reverses assignability. You may also have other properties in TableColumnConfig in a covariant position making the type ultimately invariant.

You can bypass the error by using methods instead of properties containing functions. As I learned today, methods can be optional too.

export class TableColumnConfig<T> {

    format?(val: T): string
    sort?(val1: T, val2: T): number

    constructor() {}
}

const columns: TableColumnConfig<unknown>[] = [];

// this works fine now
columns.push(new TableColumnConfig<number>());
columns.push(new TableColumnConfig<string>());

This works because variance works differently for methods. See here.

Method types both co-vary and contra-vary with their parameter types


Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • This actually works :O But I don't understand why I did use the two methods in a "contravariant position"? Is it because they can be undefined as well? – Walnussbär Oct 21 '22 at 10:14
  • @Walnussbär - when you use `T` in a parameter of a function -> that means contravariant position. There is only an exception for methods where this does not apply. – Tobias S. Oct 21 '22 at 10:16
  • Okay, I probably have to read an article about contravariance to fully understand why this is a contravariant position. Thank you so much anyway, all the syntax error is completly gone and I don't have to use any. Have a nice day! :) – Walnussbär Oct 21 '22 at 10:20
  • @Walnussbär - just be aware that is may introduce some *"unsoundness"*: Consider you have a `TableColumnConfig` and you call `format`. This will take `unknown` as a parameter `val` because that is the type of `T`. But what if the underlying instance is actually a `TableColumnConfig` which expects a `number`? It may not be able to handle the `unknown` value. That's why you got the error in the first place. Variance rules are there to warn about those errors. We *kind of* just bypassed them with the method trick ;) – Tobias S. Oct 21 '22 at 10:24
  • I think I see where you are going. So for example if I have an `Array>`, and then take out the first one and call `format()`on it, then your described error could occur, right? @Tobias S. – Walnussbär Oct 21 '22 at 12:37
  • @Walnussbär - here is a Playground that shows the issue: https://tsplay.dev/NB5Bgw. There is no error at compile time, but you might get one at runtime if you are not careful. – Tobias S. Oct 21 '22 at 14:43
  • yeha I see, that's exactly what I had in mind. But I think this is fine for me. Thank you very much for the example :) – Walnussbär Oct 21 '22 at 19:57