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

Gro*_*hni 5 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] {
    // ...
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我面临的唯一问题是,在 的实现中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]'.
    }
    // ..
  }
}
Run Code Online (Sandbox Code Playgroud)

对我来说,情况确实如此。尽管如此,我还是希望有一个完全类型安全的解决方案。

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

有可能这样做吗?或者也许有一种完全不同的方法来解决这个问题,为加载服务的使用和实现提供类型安全?

边注

对于这个简单的目的,实现可能看起来过于复杂,但原因是加载服务有一个通用基类,如下所示:

// 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] {
    // ...
  }
}
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 6

这是 TypeScript 中的一个已知痛点,请参阅microsoft/TypeScript#13995microsoft/TypeScript#24085。问题是基于控制流的类型分析不适用于泛型类型参数。

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

const k: IdentifierEnum = key;
if (k === IdentifierEnum.ID1) {
  k; // const k: IdentifierEnum.ID1
} else {
  k; // const k: IdentifierEnum.ID2
}
 
Run Code Online (Sandbox Code Playgroud)

这就是控制流分析。key现在,当is 为泛型类型时,不会发生这种情况K,但即使发生了,它也不会帮助您:

if (k === IdentifierEnum.ID1) {
  return 1; // ERROR! 
}
Run Code Online (Sandbox Code Playgroud)

这是因为,无论如何,从 TypeScript 3.8 开始,即使type 的K缩小,类型K本身也不会缩小。编译器从不说“如果keyIdentifierEnum.ID1,那么K就是IdentifierEnum.ID1”。因此,如果您希望编译器为您验证类型安全性,则不能使用这种基于控制流的实现。

TypeScript 的未来版本可能会以某种方式改善这一点,但这很棘手。一般来说,仅仅因为xtype 的值X可以缩小为 type Y,并不意味着类型X本身可以缩小。如果您有多个类型的值,这一点是显而易见的X。但无论如何,就目前而言,这是需要解决的问题。


您已经探索过类型安全性较差的方法,并且对此感到不满意:类型断言和重载签名,因此我将不再写出您将如何实现它。


在这里做一些相对类型安全的事情的唯一方法是放弃控制流分析,而使用索引操作。编译器足够聪明,能够意识到如果您有一个ttype值T和一个ktype值K extends keyof T,那么该值t[k]将是 type T[K]。在你的情况下,TDataType. 因此,您需要该类型的值来索引:

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

上述类型检查。请注意,我将属性实现为getter。你不必这样做;你可以直接写:

return {
  [IdentifierEnum.ID1]: 1,
  [IdentifierEnum.ID2]: ""
}[key];
Run Code Online (Sandbox Code Playgroud)

但是 getter 版本允许您进行更多任意计算,并且知道只有对应的计算key才会被评估。


Playground 代码链接