使用泛型的 Typescript 方法重载

Typescript method overload with generics

我搜索了几个文档和博客,但没有人介绍过这个例子..

export interface IConfigurationProvider {
    getSetting<TKey extends ConfigNames>(key: TKey): string;
    getSetting<TKey extends ConfigNames, TValue extends object>(key: TKey): TValue;
}

export enum ConfigNames {
    SomeSetting,
    AnotherSetting
}

export class ConfigurationProvider implements IConfigurationProvider {

    public getSetting<TKey extends ConfigNames>(key: TKey): string;
    public getSetting<TKey extends ConfigNames, TValue extends object>(key: TKey): TValue;
    public getSetting(key: any): any {
        // what the heck do I do here???
    }
}

因此,在大多数重载示例中,人们会做如下事情:

public getHero(name: string);
public getHero(name: string, skill: string);
public getHero(name: string, skill?: string): Hero {
    if (!skill) {
        return this.heroes.find((hero: Hero) => hero.name === name);
    }
    return this.heroes.find((hero: Hero) => hero.name === name && hero.skill === skill);
}

但在我的例子中,唯一的区别是由提供的泛型类型定义指定的 return 类型。那么,如果我无权访问 TValue,我将如何实现可以处理任何一种情况的基本实现?在 C# 中,TValue 是一个可操作项,但由于 Typescript 的重载方式,我不能去:if (typeof TValue === undefined)

如果这无法实现,那很好,但是代码会编译,如果我这样做 let configItem = config.getSetting<ConfigNames, Date>(ConfigNames.SomeSetting);,智能感知会正确地推断出我应该取回 Date 对象。因此,就像 C# 一样,有些东西可以编译,但在 运行 时不起作用,所以希望这不是其中一种情况。所以如果有人知道如何正确实现这里的基本方法,我很想知道。

更新: 对于那些要求解释我在这个用例背后的意图的人以及阅读这篇文章的其他人...

在 C# 中,我会有如下方法

public string GetSetting<T>(TKey key) where T : struct, IConvertible
{
    return CloudConfigurationManager.GetSetting(key.ToString());
}

public T GetSetting<TKey, T>(TKey key) where TKey : struct, IConvertible
{
    var settingString = CloudConfigurationManager.GetSetting(key.ToString());
    return JsonConvert.DeserializeObject<T>(settingString);
}

这允许将配置项存储为 json 对象,因此我可以将配置项存储为“11:00:00”,然后执行:var timespanSetting = config.GetSetting<ConfigNames, TimeSpan>(ConfigNames.SomeTimeSpanSetting); <-- 这是理想情况我希望能够做什么。如果我必须创建 2 个具有不同名称的方法,我会这样做,但如果我不必这样做就好了。

Typescript会被翻译成Javascript,所以typescript没有方法重载

你可以像这样使用参数对象:

getHero (param:  { name: string; skill?: string; }) {
  if (!param.skill) {
    return ... 
  }
  return ...
}

这两个方法签名都不是特别好。让我们暂时搁置过载问题。假设您将它们分解为两个单独的方法:

getSettingOne<TKey extends ConfigNames>(key: ConfigNames): string;
getSettingTwo<TKey extends ConfigNames, TValue extends object>(key: TKey): TValue;

因此,getSettingOne() 根本不使用 TKey。那真是怪了。无论您如何调用它,它都可能被推断为 {},即使您在调用它时明确地将 TKey 设置为某个值,它也不会产生任何影响。它有什么作用?你想用这个方法做什么?

那么,getSettinTwo() 至少使用 TKey 因为它是 key 参数的类型。但是现在,TValue 仅用于 return 值。由于 caller 指定类型参数的值(或者它们是从 caller 传递的值中推断出来的),此方法签名表示类似 "I will return a value of any type the caller wants"。这真的不可能正确实施。只有 never 类型可以安全地分配给每种类型...所以我将 getSettingTwo() 解释为采用 TKey 并抛出异常或从不抛出 returns 或其他东西的方法.既然那不可能是你的意思,你想用这个方法做什么?


所以,我不知道如何分别实现每个签名(抛出异常除外),我真的不知道如何将它们实现为重载函数.但如果问题只是 "how can I make an implementation signature for this",答案是实现签名只需要至少与所有调用签名一样宽松。你也可以给它类型参数。因此,以下内容将进行类型检查,并且 "give you access to TKey" 至少与任一调用签名一样多:

public getSetting<TKey extends ConfigNames>(key: ConfigNames): string;
public getSetting<TKey extends ConfigNames, TValue extends object>(key: TKey): TValue;
public getSetting<TKey extends ConfigNames, TValue extends object>(key: TKey): TValue | string {
  throw new Error(); // useless
}

但是仍然没有很好的方法来实现那个东西(这就是为什么我只是在那里抛出一个错误)。我的建议是让您重新审视您的方法签名并将它们更改为您实际的意思。我认为您在问题中没有提供足够的信息让任何人帮助您做到这一点。也许您想使用此信息编辑您的问题或创建一个新问题?在那之前,祝你好运!


更新:

好的,现在我知道你想做什么了。看起来您想以两种方式之一调用该函数...一种只获取 JSON 字符串,另一种解析该 JSON 字符串并断言它是某种调用者指定的类型.

注意后面的调用根本不是类型安全的...调用者可以调用

let configItemDate = config.getSetting<ConfigNames, Date>(ConfigNames.SomeSetting);

let configItemRegExp = config.getSetting<ConfigNames, RegExp>(ConfigNames.SomeSetting);

并且编译器会认为 configItemDateDateconfigItemRegExpRegExp,即使其中最多有一个是真的。请记住 type system is erased at runtime,因此两个调用都被转换为 config.getSetting(ConfigNames.SomeSetting)... 这意味着 configItemDateconfigItemRegExp 在运行时将是相同的值。

有时您确实想在 TypeScript 中进行不安全的类型断言,但最好小心谨慎。拥有一种声称 return 调用者想要的任何类型的方法,但无法验证它的风险比我想要的要高。但是,无论如何,假设您想那样做。

用一个方法完成你想要的事情的唯一方法是提供一些方法在运行时来区分return是一个字符串的调用和称 return 为对象。这意味着您需要引入一个新参数,其值控制这个:

getSetting(key: ConfigNames, parseJSON?: true);

如果您将 parseJSON 参数作为 true 传递,您将获得一个对象,如果您将其省略,您将获得 JSON 字符串。它可以(可能)通过这样的重载来完成:

public getSetting(key: ConfigNames): string;
public getSetting<TValue extends object>(key: ConfigNames, parseJSON: true): TValue;
public getSetting<TValue extends object>(key: ConfigNames, parseJSON?: true): TValue | string {
  const json = CloudConfigurationManager.GetSetting(key.toString());       
  return (parseJSON) ? JSON.parse(json) : json;
}

但在这一点上,过载并没有给你带来太多好处。更直接的是两个必须完全不同的方法,如:

getSetting(key: ConfigNames): string;
getSettingAs<V extends object>(key: ConfigNames): V;

将单独实施。


我再次重申 getSettingAs() 不是类型安全的,因为调用者可以将任何东西插入 V 并且它会安抚编译器而不会在运行时影响任何东西。

举个例子,我真诚地怀疑任何正常的 JSON 解析器是否会生成有效的 JavaScript Date 对象。您可以制作一个自定义 JSON 解析器来执行此操作。如果您开始自定义 JSON 解析器,它会导致另一种可能的方法 getSetting()... 传入一个可选的 JSON 解析函数,该函数已知会转换为所需类型:

public getSetting(key: ConfigNames): string;
public getSetting<V extends object>(key: ConfigNames, jsonParser: (json: string)=>V): V;
public getSetting<V extends object>(key: ConfigNames, jsonParser?: (json: string)=>V): V | string {
  const json = CloudConfigurationManager.GetSetting(key.toString());       
  return (jsonParser) ? jsonParser(json) : json;
}

这现在是类型安全的,但依赖于调用者传入有效的 JSON 解析器。


希望其中一个想法对您有所帮助。请注意我是如何从所有这些中完全删除 TKey 的,因为您似乎没有使用它。另请注意,您提供的 enum 是一个数字,但您似乎期望 key 可能是一个字符串?如果我是你,我会专注于让你的代码在运行时工作(可能用纯 JavaScript 编写),然后尝试为其添加类型。或者,如果没有,也许上述类型之一对您有用。无论如何,祝你好运!