typescript 使用相关参数变量的类型判别类型的并集

bzzcjhmw  于 2023-06-24  发布在  TypeScript
关注(0)|答案(1)|浏览(109)

我有以下类型和一个枚举:

enum TableNames {
  clients = "clients",
  products = "products"
}

interface IClient {
  client: string;
  location: string;
  type: string;
}
interface IProduct {
  product: string;
  family: string;
  subfamily: string;
}

type TSelector<T> = {
  [K in keyof T]: T[K][];
};

type TClientSelector = TSelector<IClient>;
type TProductSelector = TSelector<IProduct>;

这两个类基于TClientSelectorTProductSelector初始化一个对象。

export class BaseClientSelector implements TClientSelector {
  client: string[] = [];
  type: string[] = [];
  location: string[] = [];
}

export class BaseProductSelector implements TProductSelector {
  product: string[] = [];
  family: string[] = [];
  subfamily: string[] = [];
}

使用变量name分派类型为TClientSelectorTProductSelector的对象的工厂函数。

const getEmptySelectorObject = (name: TableNames): TClientSelector | TProductSelector => {
  if (name === TableNames.clients) {
    return new BaseClientSelector();
  } else {
    return new BaseProductSelector();
  }
};

问题就在这里。考虑以下函数:

export const getSelectorData = (
  arrayOfObj: (IClient | IProduct)[],
  name: TableNames
): TClientSelector | TProductSelector => {
  const selectorData: TClientSelector | TProductSelector = getEmptySelectorObject(name);

  for (const col in selectorData) {
    selectorData[col] = Array.from(
//  ^^^^^^^^^^^^^^^^^ TS7053
      new Set(arrayOfObj.map((obj: IClient | IProduct) => obj[col]))
//                                                        ^^^^^^^^ TS7053
    ).sort((a, b) => String(a).localeCompare(String(b)));
  }

  return selectorData;
};

如果name的类型是TableNames.clients,那么selectorData的类型必须是TClientSelector。但是,由于TypeScript无法知道selectorData将具有哪个类型,因为我不知道如何向它表达这种关系,因此当迭代selectorData的键时,它默认将col分配给类型string。这提示编译器告诉我TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'TSelector<IClient> | TSelector<IProduct>'.   No index signature with a parameter of type 'string' was found on type 'TSelector<IClient> | TSelector<IProduct>'
类似的情况发生在Map中,obj可以是IClientIProduct,所以col不能用于索引它。
我尝试过的解决方案无法说服我:

  • 带类型保护的代码重复。
  • 正在添加显式any % s。
  • 函数重载(一个用于TClientSelector,另一个用于TProductSelector

我相当肯定这可以使用泛型来解决,但我无法理解它们。
我找到了这个答案的jcalz https://stackoverflow.com/a/72294288,它使用了一个 * 分布式对象类型 *,有些东西告诉我这可能是答案,但我不能适应这个解决方案我的问题。

zf2sa74q

zf2sa74q1#

现在,甚至getSelectorData()的调用端也是一个问题,因为两个参数之间的相关性没有表示出来。您的呼叫签名如下所示:

declare const getSelectorData: (
  arrayOfObj: (IClient | IProduct)[], name: TableNames
) => TClientSelector | TProductSelector;

所以它允许这样:

getSelectorData([{ client: "a", location: "b", type: "c" }],
  TableNames.products
) // no error, but there should be.

您可以使用REST参数元组的区分并集来强制这种关联:

declare const getSelectorData: (
  ...[arrayOfObj, name]:
    [IClient[], TableNames.clients] |
    [IProduct[], TableNames.products]
) => TClientSelector | TProductSelector;

getSelectorData(
  [{ client: "a", location: "b", type: "c" }], // error!
  TableNames.products
);
getSelectorData(
  [{ client: "a", location: "b", type: "c" }], // okay
  TableNames.clients
)

但这只会从呼叫者的一方帮助。
实现仍然有错误,这是因为TypeScript不太支持“相关的联合”类型,在这种情况下,对于某个联合类型表达式的每一个可能的缩小,需要分析一次单个代码块,如microsoft/TypeScript#30581中所讨论的。
在你的函数实现中,你会希望编译器注意到,如果函数参数的类型是[IClient[], TableNames.clients],那么一切都很好,如果它们的类型是[IProduct[], TableNames.products],那么它们也很好,所以它们总体上都很好。但是TypeScript并不做任何类型的“分布式”控制流分析(甚至不像microsoft/TypeScript#25051中建议的那样在“选择加入”的基础上)。相反,编译器将每个union类型的值视为独立的,因此即使调用签名阻止传入[IClient[], TableNames.products],编译器在实现中也没有意识到这一点。所以它抱怨。
官方推荐的修复此类问题的方法在microsoft/TypeScript#47109中描述。这个想法是从联合切换到[泛型] https://www.typescriptlang.org/docs/handbook/2/generics.html),这些泛型被约束到一些基本接口的键。然后你必须重写所有的类型和操作,在这个基本接口上重写mapped types,在这个接口上重写泛型indexes
但这样做通常是一个重大的重构,可能很难遵循。如果您想要方便,您应该在仔细检查您的实现是否良好之后Assert或使用any类型。治疗可能比疾病更糟糕。
你是法官:
在你的情况下,我会重构如下。下面是表示您想要抽象的关系的基本接口:

interface TableMap {
  [TableNames.clients]: IClient,
  [TableNames.products]: IProduct
}

现在,您可以将TSelector重写为泛型,覆盖TableMap的 * key *,如下所示:

type ArrayProps<T> = { [K in keyof T]: T[K][] }
type TSelectorMap<K extends TableNames> = { [P in K]: ArrayProps<TableMap[P]> }
type TSelector<K extends TableNames> = TSelectorMap<K>[K]

实用程序类型ArrayProps<T>只是让你很容易表示你想要的TSelector<K>。请注意,TSelector<K>使用其所有键索引到Map类型,这使其成为ms/TS#47109中创造的 * 分布式对象类型 *。我将TSelectorMap<K>命名为Map类型部分,因为它在后面会很有用。
您的TClientSelectorTProductSelector类型可以稍微重写为:

type TClientSelector = TSelector<TableNames.clients>;
type TProductSelector = TSelector<TableNames.products>;

你可以验证它们和以前一样。
现在,为了使其工作,您的getEmptySelectorObject()函数必须以相同的方式泛型。如果它返回一个联盟,那么我们将陷入困境。由于返回类型将是TSelector<K>,一个指向TSelectorMap<K>的索引,其键类型为K,那么说服编译器这样做的方法是实际创建一个TSelectorMap<K>类型的值并索引到它中。下面是一个TSelectorMap<TableNames>(任何K extends TableNamesTSelectorMap<K>子类型):

const baseSelectors: TSelectorMap<TableNames> = {
  get [TableNames.clients]() { return new BaseClientSelector() },
  get [TableNames.products]() { return new BaseProductSelector() }
}

请注意,我使用的是getter methods,因此实际代码不会运行,直到有人尝试访问该属性。现在getEmptySelectorObject()函数实现只需要在baseSelectors中查找name

const getEmptySelectorObject = <K extends TableNames>(name: K): TSelector<K> => {
  const _baseSelectors: TSelectorMap<K> = baseSelectors;
  return _baseSelectors[name];
};

差不多吧编译器无法“看到”baseSelectors[name]TSelectorMap<K>[K]类型。相反,它只“看到”TSelectorMap<TableNames>[K],并抱怨。但是它确实知道TSelectorMap<TableNames>TSelectorMap<K>的子类型,所以我们可以通过注解一个更宽类型的新变量并赋值来安全地“向上转换”。
最后,getSelectorData()

const getSelectorData = <K extends TableNames>(
  arrayOfObj: TableMap[K][],
  name: K
) => {
  const selectorData = getEmptySelectorObject(name);

  for (const col in selectorData) {
    selectorData[col] = Array.from(
      new Set(arrayOfObj.map(obj => obj[col]))
    ).sort((a, b) => String(a).localeCompare(String(b)));
  }
   
  return selectorData;
};

它已经变得通用了,而且很有效。还有一点类型的安全孔你仍然可以调用getSelectorData(),其中K是完整的TableNames联合体,for循环不会检查你是否将错误的属性分配给错误的列(col或多或少只是keyof TSelector<K>,但你真的需要一个通用的P extends keyof TSelector<K>)。但这是一阶重构。
所以,这对你来说值得吗?我不能说,但我更愿意维护我个人理解的代码,而不是Stack Overflow中的某个人给我的一堆GitHub问题的链接。
Playground链接到代码

相关问题