从枚举参数推断Typescript函数返回类型

时间:2020-04-02 16:19:29

标签: typescript typescript-typings

我想创建一个加载服务,该服务返回枚举中定义的ID的正确类型的数据。我所做的看起来像这样:

enum IdentifierEnum {
  ID1 = 'ID1', 
  ID2 = 'ID2'
}

interface DataType {
  [IdentifierEnum.ID1]: number,
  [IdentifierEnum.ID2]: string
}

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    // ...
  }
}

使用此方法,可以在使用加载服务时正确推断类型:

const loadingService: LoadingService = new LoadingService();
const data1 = loadingService.loadData(IdentifierEnum.ID1); // type: number
const data2 = loadingService.loadData(IdentifierEnum.ID2); // type: string

我面临的唯一问题是,在loadData的实现中,类型参数K仅被推断为IdentifierEnum。因此,以下操作将无效:

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    if (key === IdentifierEnum.ID1) {
      return 1; // Error: Type '1' is not assignable to type 'DataType[K]'.
    }
    // ..
  }
}

我确实是这样。尽管如此,我还是很乐意为此提供一个完全类型安全的解决方案。

我已经尝试过重载该函数,但这给我留下了一个问题,我仍然必须提供一个实现签名,该签名要么太具体(如上述),要么太笼统,这又再次删除了我想要的类型安全性。转换返回值也是如此。我基本上需要的是一种真正对输入值进行类型检查的方法,而不仅仅是检查其值。

是否可以这样做?还是可以通过另一种完全不同的方式来解决此问题,从而为加载服务的使用和实现提供类型安全?

旁注

出于这个简单的目的,实现似乎过于复杂,但这是因为加载服务有一个通用基类,如下所示:

// Base class
abstract class AbstractLoadingService<E extends string | number | symbol, T extends {[K in E]: any}> {
  abstract loadData<K extends E>(key: K): T[K];
}

// Implementation
class LoadingService extends AbstractLoadingService<IdentifierEnum, DataType> {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    // ...
  }
}

1 个答案:

答案 0 :(得分:1)

这是TypeScript中的已知痛点,请参见microsoft/TypeScript#13995。问题是control-flow based type analysis不适用于通用类型参数。

如果将key声明为IdentifierEnum类型,则检查if (key === IdentifierEnum.ID1) {...}会将key块内{...}的类型缩小为{ {1}}:

Identifier.ID1

那是控制流分析。现在,const k: IdentifierEnum = key; if (k === IdentifierEnum.ID1) { k; // const k: IdentifierEnum.ID1 } else { k; // const k: IdentifierEnum.ID2 } 的类型为key时不会发生这种情况,但是即使发生了,也无济于事:

K

这是因为从TypeScript 3.8开始,即使if (k === IdentifierEnum.ID1) { return 1; // ERROR! } 类型的 value 变窄,类型K本身也是 not 。编译器永远不会说“如果Kkey,那么IdentifierEnum.ID1K”。因此,如果您希望编译器为您验证类型安全,则不能使用这种基于控制流的实现。

将来的TypeScript版本有可能以某种方式使它变得更好,但这很棘手。通常,仅可以将类型IdentifierEnum.ID1的值x缩小为类型X,这并不意味着类型Y本身可以缩小。如果您有多个X类型的值,这很明显。但是无论如何,目前来说,这是可以解决的。


您已经探索了这种类型安全性较差的方法,并且对此类型感到不满意:类型声明和重载签名,因此我将省去写出如何实现的方法。


在这里做相对安全的事情的唯一方法是放弃控制流分析,而使用索引操作。编译器足够聪明,可以意识到,如果您拥有类型X的值t和类型T的值k,则值K extends keyof T将类型为t[k]。在您的情况下,T[K]T。因此,您需要该类型的值才能索引到

DataType

以上类型检查。请注意,我将属性实现为getters。您不必这样做;您可能刚刚写过:

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    return {
      get [IdentifierEnum.ID1]() { return 1; },
      get [IdentifierEnum.ID2]() { return "" }
    }[key]; // okay
  }
}

但是使用getter版本,您可以进行更多的任意计算,并且知道只有与return { [IdentifierEnum.ID1]: 1, [IdentifierEnum.ID2]: "" }[key]; 相对应的计算才会被评估。


好的,希望能有所帮助;祝你好运!

Playground link to code