【问题标题】:What I can use in typescript instead wildcard我可以在打字稿中使用什么而不是通配符
【发布时间】:2021-02-15 06:53:38
【问题描述】:

我有一个接口ColumnDef。我需要创建 2 个方法:getValue 将返回 C 类型,getComponent 将输入 arg 类型为 C。而且效果很好!

interface ColumnDef<R, C> {
  getValue: (rowData: R) => C;
  getComponent: (cellData: C) => ReactElement
}

然后我创建了一个数组ColumnDef。我不需要为每一列说第二种类型。 (我想使用泛型类型,比如在 java 中:ColumnDef&lt;R, ?&gt;[])我只需要在getValuegetComponent 中匹配每一列的类型。我尝试使用“任何”类型,例如 ColumnDef&lt;R, any&gt;[];,但它破坏了类型匹配,例如以下代码:

interface ColumnDef<R> {
  getValue: (rowData: R) => any;
  getComponent: (cellData: any) => ReactElement
}

我想这样使用它:

const columns: ColumnDef<R, any>[] = [
    { getValue: (rowData: R) => rowData.name, getComponent: (name: string) => Label(name)},
    { getValue: (rowData: R) => rowData.size, getComponent: (size: number) => NumberBox(size)}  
  ]

附加的最小可重现示例:

import * as React from 'react';
import { ReactElement } from 'react';

interface TestData {
  name: string,
  size: number
}

interface ColumnDef<R, C> {
  getValue: (rowData: R) => C;
  getComponent: (cellData: C) => ReactElement
}
function NumberBox(size: number): ReactElement {
  return <div>{size}</div>
}

const columns: ColumnDef<TestData, any>[] = [
  { getValue: (rowData: TestData) => rowData.name, getComponent: (name: number) => NumberBox(name)},  // It is incorrect: name is not number!
  { getValue: (rowData: TestData) => rowData.size, getComponent: (size: number) => NumberBox(size)}  // It is correct: size is number!
]

console.log(columns)

【问题讨论】:

  • (C: cellData) =&gt; 应该是(cellData: C) =&gt;,我猜?理想情况下,示例代码应在 IDE 中进行测试并构成 minimal reproducible example,以便其他人可以更轻松地帮助您。
  • 好的,您已经修复了那里的错误,但是您能否在minimal reproducible example 上工作,显示您打算如何创建和使用这些数组之一? TypeScript 缺乏对existential types 的直接支持(这基本上是Java 泛型中的通配符),因此您必须以某种方式解决它。 any 的使用对于那些宁愿获得便利而不是严格类型安全的人来说通常已经足够了。有更安全的选择,但我想了解更多关于用例的信息,然后再用解决方案朝一个方向走得太远。
  • @jcalz 谢谢!我向问题添加了更多信息
  • const columns: ColumnDef&lt;R, any&gt;[] 中的R 是什么? minimal reproducible example 应该是我可以放入 IDE 的东西(最好是像 The TypeScript Playground 这样的独立的,看看发生了什么。如果我现在这样做,我将面临 R 未定义的事实,并且LabelNumberBox 也不是。无论如何,我可能会回答这个问题,但如果有一些我可以实际测试的东西会很好。
  • @jcalz 感谢您的耐心等待。我添加了最小的可重现示例。

标签: reactjs typescript typescript-generics


【解决方案1】:

TypeScript 不直接支持 existential types,这就是 Java 泛型中通配符的本质。存在类型是不同于 TypeScript 中可用的泛型类型参数。

在 TypeScript 中,如果您说 interface ColumnDef&lt;R, C&gt; {...},则表示此定义适用于 RC所有规范。要有意义地谈论ColumnDef&lt;R, C&gt; 的实例,您需要同时指定RC

但是您正在寻找的是像interface ColumnDef&lt;R, exists C&gt; {...} 这样的类型,它适用于R所有 规范,但适用于C一些未知 规范.要谈论ColumnDef&lt;R, exists C&gt; 的实例,您需要指定 R,但您只需要知道C 有一个实例。

由于 TypeScript 对此没有直接支持,因此您必须采取各种变通方法。


为了具体谈谈这个,我将制作几个ColumnDef&lt;R, C&gt; 对象和一个失败的对象:

const cd1: ColumnDef<number, string> = {
  getValue(r) { return r.toFixed(0) },
  getComponent(c) { c.toUpperCase(); return reactElement; }
}
const cd2: ColumnDef<number, boolean> = {
  getValue(r) { return r > 3 },
  getComponent(c) { (c === true); return reactElement; }
}
const cdBad = {
  getValue(r: number) { return r.toFixed(0) },
  getComponent(c: boolean) { (c === true); return reactElement; }
}

最简单的解决方法是使用any 代替exists C。这将允许您需要允许的任何内容,但不会禁止一些错误。类似cdBad 的内容将被接受:

const columnDefArray: ColumnDef<number, any>[] = 
  [cd1, cd2, cdBad]; // oops, no error, but ?‍♂️

作为一种解决方法,您可以确保所有 ColumnDef&lt;R, C&gt; 实例都是通过辅助函数或类构造函数创建的:

class ColumnDefClass<R, C> {
  private onlyWayToMakeItIsWithTheCtor = true;
  constructor(
    public getValue: (rowData: R) => C,
    public getComponent: (cellData: C) => ReactElement) {
  }
}

const columnDefClassArray: ColumnDefClass<number, any>[] = [
  new ColumnDefClass(cd1.getValue, cd1.getComponent),
  new ColumnDefClass(cd2.getValue, cd2.getComponent),
  new ColumnDefClass(cdBad.getValue, cdBad.getComponent) // error!
]

这里因为ColumnDefClass 有一个private 属性,所以如果不调用构造函数,您将无法轻松创建它的实例。这将使您更难违反您的约束。

然后,一旦你有一个 ColumnDef&lt;R, any&gt; 数组,你就可以很容易地映射它:

const numToElements = (n: number) => columnDefClassArray.map(
  <C>(cd: ColumnDef<number, C>) => cd.getComponent(cd.getValue(n))
);
// const numToElements: (n: number) => ReactElement[]

我建议使用上面带有C 的通用回调,以使该回调比any 给您的类型更安全。


如果您愿意,您实际上可以使用 curried 泛型函数在 TypeScript 中对存在类型进行编码(如 in this comment 所示):

type SomeColumnDef<R> = <T>(cb: <C>(columnDef: ColumnDef<R, C>) => T) => T;

这里,SomeColumnDef&lt;R&gt; 是一个通用函数,它为某些CColumnDef&lt;R, C&gt; 上调用回调。直观地说,由于泛型函数的调用者是必须指定泛型参数的调用者,因此通过将值表示为 Promise 类似的事物,它调用采用该值的回调,您正在更改指定参数的人。

您可以轻松地将有效的ColumnDef&lt;R, C&gt; 转换为SomeColumnDef&lt;R&gt;

const toSomeColumnDef = <R, C>(columnDef: ColumnDef<R, C>): SomeColumnDef<R> =>
  cb => (cb(columnDef));

并以这种方式获取您的数组:

const columnDefArray = [toSomeColumnDef(cd1), toSomeColumnDef(cd2)];
// const columnDefArray: SomeColumnDef<number>[]

同时也禁止不良行为:

const badColumnDefArray = 
  [toSomeColumnDef(cd1), toSomeColumnDef(cd2), toSomeColumnDef(cdBad)] // error!

然后,map-ping 该数组有点模糊,因为您必须有一个额外的嵌套回调层,但它确实有效:

const numToElements = (n: number) => columnDefArray.map(someColumnDef => someColumnDef(
  <C>(cd: ColumnDef<number, C>) => cd.getComponent(cd.getValue(n))
));
// const numToElements: (n: number) => ReactElement[]

最后,您可以尝试依靠 TypeScript 的类型推断将C 视为类型本身的数组,并为C 中的每个索引I 生成相应的数组类型以ColumnDef&lt;R, C[I]&gt; 为元素。这是由mapping over array/tuple types 启用的。

但这开始越来越接近 TypeScript 中类型推断的限制。柯里化很奇怪,但推断还可以:

const curriedArray = <R>() => <C extends readonly any[]>(
  ...columnDefs: { [I in keyof C]: ColumnDef<R, C[I]> }
) => columnDefs;

const curriedColumnDefArray = curriedArray<number>()(cd1, cd2);
const badCurriedColumnDefArray = curriedArray<number>()(cd1, cd2, cdBad); // error!

试图使用更直接的东西是令人难以置信的丑陋陷入困境,我不打算解释太多:

type GetR<T extends ColumnDef<any, any>[]> = { [K in keyof T]: T[K] extends ColumnDef<infer R, any> ? R : never }[number];
type GetCs<T extends ColumnDef<any, any>[]> = Extract<{ [K in keyof T]: T[K] extends ColumnDef<any, infer C> ? C : never }, any[]>;
type _ValidateCDA<R, Cs extends any[]> = Extract<{ [K in keyof Cs]: ColumnDef<R, Cs[K]> }, ColumnDef<any, any>[]>;
type ValidateColumnDefArray<T extends ColumnDef<any, any>[]> = _ValidateCDA<GetR<T>, GetCs<T>>;

const asColumnDefArray = <T extends ValidateColumnDefArray<T>>(...arr: T) => arr;
const columnDefArray = asColumnDefArray(cd1, cd2);
const badColumnDefArray = asColumnDefArray(cd1, cd2, cdBad); // error!

呸。

但即使这样可行,对数组进行迭代也很糟糕,因为在元组上的 map() 回调最终需要大量的事物联合。首先将其扩大到ColumnDef&lt;R, any&gt;[] 会更容易:

const numToElements = (n: number) => (columnDefArray as ColumnDef<number, any>[]).map(
  <C>(cd: ColumnDef<number, C>) => cd.getComponent(cd.getValue(n))
);

所以你去。有不同的方法可以进行,但没有一个是完美的。就我个人而言,我可能会坚持使用any,因为生命很短暂。但这取决于你。

Playground link to code

【讨论】:

    猜你喜欢
    • 2019-08-03
    • 1970-01-01
    • 2016-02-09
    • 2014-07-31
    • 2014-05-04
    • 2021-03-27
    • 2016-04-08
    • 2016-03-17
    • 2017-05-25
    相关资源
    最近更新 更多