简体   繁体   中英

What type declaration to use for a generic T that may or may not be hierarchical?

I'm extending a React table (creatively called "Table") that takes a generic data array, along with some column-config data, and renders a table. The declaration looks like this:

export class Table<T> extends React.Component<TableProps<T>, any>

The non-optional parts of TableProps:

export interface TableProps<T> {
    columnConfigs: TableColumnConfig<T>;
    tableStore: TableStore<T>;
}

And TableStore has only one required property:

export class TableStore<T> {
    public data: T[];
}

TableColumnConfigs isn't terribly important, since the T is only used in an optional property:

export interface TableColumnConfigs<T> {
    render?: (data: T) => JSX.Element
}

This <Table> has been used several places already, and I'm adding a feature to it: optional hierarchical data, so we can stop using Kendo's TreeList (which is very, very featureful, but a bit slow to load in our environment).

My problem: I want to declare that T, the data type used in the table and supplied by someone outside the class, may have a particular property that makes the type have recursive children, but is not required to.

I've tried this:

export interface ChildData<T> {
    children?: (T & ChildData<T>)[]
}
export class Table<T extends ChildData<T>> extends React.Component<TableProps<T>, any> ...

This doesn't work, because that external class doesn't define a property called "children".

The problem is that TypeScript 2.8 doesn't like it: when I actually use Table somewhere, it whines that T isn't implementing ChildData. (I'm compiling using TS 2.3 at the moment, but I'm gonna upgrade past 2.4, at some point, real soon, I swear. Eventually. At any rate, I definitely don't want to be blocked from upgrading. I know it doesn't work on 2.8 because my IDE is running Intellisense using 2.8, but I'm running the 2.3 compiler.)

I've also tried declaring using union and intersection types, but they simply don't compile, even under TS 2.3:

export class Table<T & ChildData<T>> extends React.Component<TableProps<T>, any> ...
export class Table<T | ChildData<T>> extends React.Component<TableProps<T>, any> ...

Is there some way to say "I have a generic parameter T, that may implement a ChildData, but isn't required to?" My <Table> component is already being used in several places elsewhere, and it's going to be used by people that aren't me. I don't want to require consumers of Table to also extend their T types by a "children" property that they aren't using (since the hierarchical feature of the table is entirely optional).

After some futzing around, I eventually realized that ChildData only matters in one place: inside the "data" property of TableStore, which is where all of the relevant data lives. So I redefined TableStore:

export class TableStore<T> {
    public data: (T & ChildData<T>)[];
    constructor(input: T[]) { this.data = input; }
}

Apparently, you can't use & and | in generic parameters, but "extends" is okay. And, if you keep passing generic parameters around, TypeScript eventually loses track of the fine details of the generic. The act of taking T in the class, and using (T & somethingElse) in a property, allows me to directly assign a T to a (T & somethingElse).

I'm assuming that, in the same way that TypeScript 2.4 tightened up generics, a future release of TypeScript will disallow the shenanigans I'm currently getting away with.

Mind you, the implementation is a bit wonky, because I have to cast to <T & ChildData<T>> in several places inside <Table>, as well as in the consuming class as I'm generating the TableStore's data.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM