简体   繁体   English

Typescript中错误的自动返回类型扣除

[英]Wrong automatic return type deduction in Typescript

In a settings class I have a method to get a value, with an optional default value to return if the given key cannot be found:在设置 class 中,我有一个获取值的方法,如果找不到给定的键,则返回一个可选的默认值:

    /**
     * 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;
    }

This implementation gives me return types I did not expect.这个实现给了我没想到的返回类型。 Consider this call:考虑这个电话:

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

The variable removeIdleTime is not of type number as I would expect, but of type 60 .变量removeIdleTime不是我期望的类型 number ,而是类型60 I can explicitly use <number> as template/generic parameter to get and the result will then be ok, but it would be way more cool to have Typescript deduce the right type.我可以显式使用<number>作为模板/通用参数来get结果,然后结果就可以了,但是让 Typescript 推断出正确的类型会更酷。 What must be changed to accomplish that?必须改变什么才能做到这一点?

Update更新

I just found the description about type widening in Typescript (I didn't know the correct term at the time of writing this question).我刚刚在 Typescript 中找到了关于类型扩展的描述(在写这个问题时我不知道正确的术语)。 It turns out that the type is widended when assigning the result of get to a mutable variable.事实证明,将get的结果分配给可变变量时,类型被扩大了。 Otherwise it stays a literal type.否则它保持文字类型。

While this is interesting information, it doesn't help with this question, because linters will usually convert any let to const if they are not changed after the initial assignment.虽然这是一个有趣的信息,但它对这个问题没有帮助,因为如果在初始分配后它们没有更改,linters 通常会将任何let转换为const

Solution解决方案

Thanks to @Etheryte I found a solution: there's a way to enforce type widening by using conditional types.感谢@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;

which can be used so (note the only change, for the return type):可以这样使用(注意唯一的变化,返回类型):

    /**
     * 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;
    }

This also works for enums, where the conditional type returns number or string, depending on the base type of the enum.这也适用于枚举,其中条件类型返回数字或字符串,具体取决于枚举的基本类型。


Previous Answer上一个答案

This behavior is by design.此行为是设计使然。 Literal values that are assigned to constant targets keep their literal type.分配给常量目标的文字值保持其文字类型。 This follows the principle of always storing the most narrow type.这遵循始终存储最窄类型的原则。 In situations where values can be changed (eg by assigning a literal to a mutable variable) the Typescript transpiler widens the type to allow for other values than the initial literal.在值可以更改的情况下(例如,通过将文字分配给可变变量),Typescript 转换器会扩展类型以允许除初始文字之外的其他值。 You can read a bit more about that in the article Literal Type Widening in TypeScript .您可以在 TypeScript 中的文字类型加宽一文中阅读更多相关信息。

There's no way (I know of) to force type widening, so I see 3 possible ways here:没有办法(我知道)强制类型加宽,所以我在这里看到 3 种可能的方法:

  1. Use a mutable target when you assign the result from the get call.分配get调用的结果时使用可变目标。 This might be problematic, because linters will try to "optimize" the variable to become immutable, if there are no other assignments to it.这可能会有问题,因为如果没有其他分配给它,linters 将尝试“优化”变量使其变得不可变。

  2. Add an explicit type annotation to the target, like:向目标添加显式类型注释,例如:

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

  1. Specify the generic parameter explicitly:明确指定泛型参数:

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

or要么

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

All these ways are not really a solution to my question, though.但是,所有这些方法并不能真正解决我的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM