【问题标题】:Wrong automatic return type deduction in TypescriptTypescript中错误的自动返回类型推导
【发布时间】:2021-04-13 15:26:44
【问题描述】:

在设置类中,我有一个获取值的方法,如果找不到给定的键,则返回一个可选的默认值:

    /**
     * Returns the value stored at the given key, which can have the form `qualifier.subKey`.
     *
     * @param key The key to look up.
     * @param defaultValue An optional value to be returned if the there's no value at the given key or the key doesn't
     *                     exist at all.
     *
     * @returns If a return value is given the return type is the same as that of the default value. Otherwise it's
     *          either undefined or the same as the type found at the given key.
     */
    public get<T>(key: string, defaultValue: T): T;
    public get(key: string): any;
    public get<T>(key: string, defaultValue?: T): T | undefined {
        const { target, subKey } = this.objectForKey(key, false);
        if (!target || !subKey) {
            return defaultValue;
        }

        return target[subKey] as T ?? defaultValue;
    }

这个实现给了我没想到的返回类型。考虑这个调用:

const removeIdleTime = settings.get("workers.removeIdleTime", 60);

变量removeIdleTime 不是我所期望的数字类型,而是60 类型。我可以明确地将&lt;number&gt; 用作get 的模板/通用参数,然后结果就可以了,但是让Typescript 推断出正确的类型会更酷。必须改变什么来实现这一点?

更新

我刚刚在 Typescript 中找到了关于 type widening 的描述(在写这个问题的时候我不知道正确的术语)。事实证明,将get 的结果分配给可变变量时,类型被扩展了。否则它保持为文字类型。

虽然这是有趣的信息,但它对这个问题没有帮助,因为如果在初始分配后没有更改任何 let,linter 通常会将它们转换为 const

【问题讨论】:

  • 键是一组有限的值吗?如果是这样,那么您希望泛型为 K extends keyof SomeType ,其中键为 K,默认值为 SomeType[K]。
  • 也许我理解错了,但问题的症结在于如何从文字类型(60)到基本类型(number)?
  • 有趣,不确定在哪个版本中发生了变化,但之前要“触发”文字类型推断,您必须在泛型类型约束 github.com/microsoft/TypeScript/pull/24310 中具有原始类型。但现在已经不是这样了typescriptlang.org/play?#code/…
  • @Etheryte 无需提交错误报告。行为改变是有意的。请参阅 Aleksey 的链接,其中 Anders 解释了其背后的基本原理。
  • 在某些情况下会出现问题:假设您从存储中获取了一个数字值并使用默认值。文字类型是 1,当你测试它的 2 时,你会得到一个警告,这个比较总是错误的,因为 1 和 2 没有重叠。

标签: typescript


【解决方案1】:

解决方案

感谢@Etheryte,我找到了一个解决方案:有一种方法可以通过使用条件类型来强制类型扩展。

export type ValueType<T> = T extends string
    ? string
    : T extends number
        ? number
        : T extends boolean
            ? boolean
            : T extends undefined
                ? undefined
                : [T] extends [any]
                    ? T
                    : object;

可以这样使用(注意唯一的变化,返回类型):

    /**
     * Returns the value stored at the given key, which can have the form `qualifier.subKey`.
     *
     * @param key The key to look up.
     * @param defaultValue An optional value to be returned if the there's no value at the given key or the key doesn't
     *                     exist at all.
     *
     * @returns If a return value is given the return type is the same as that of the default value. Otherwise it's
     *          either undefined or the same as the type found at the given key.
     */
    public get<T>(key: string, defaultValue: T): ValueType<T>;
    public get(key: string): any;
    public get<T>(key: string, defaultValue?: T): T | undefined {
        const { target, subKey } = this.objectForKey(key, false);
        if (!target || !subKey) {
            return defaultValue;
        }

        return target[subKey] as T ?? defaultValue;
    }

这也适用于枚举,其中条件类型返回数字或字符串,具体取决于枚举的基本类型。


上一个答案

此行为是设计使然。分配给常量目标的文字值保持其文字类型。这遵循始终存储最窄类型的原则。在可以更改值的情况下(例如,通过将文字分配给可变变量),Typescript 转译器会扩大类型以允许初始文字以外的其他值。您可以在文章Literal Type Widening in TypeScript 中阅读更多相关信息。

没有办法(我知道)强制类型扩大,所以我在这里看到了 3 种可能的方法:

  1. 在分配get 调用的结果时使用可变目标。这可能是有问题的,因为如果没有其他赋值,linter 会尝试“优化”该变量以使其成为不可变的。

  2. 为目标添加显式类型注释,如:

const removeIdleTime: number = settings.get("workers.removeIdleTime", 60);

  1. 明确指定泛型参数:

const removeIdleTime = settings.get&lt;number&gt;("workers.removeIdleTime", 60);

const removeIdleTime = settings.get("workers.removeIdleTime", 60 as number);

不过,所有这些方法都不能真正解决我的问题。

【讨论】:

    猜你喜欢
    • 2019-08-21
    • 1970-01-01
    • 2020-07-29
    • 2021-12-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多