打字稿可以处理不基于函数参数的动态返回类型吗?

时间:2020-12-21 16:25:26

标签: typescript

我知道如何使用参数类型处理动态类型检查,但我坚持使用这个。这可能是不可能的,但我不能再直接思考了,所以欢迎任何帮助!

给定代码:

class DefaultClass {
    defaultProp: number;
    constructor(num: number) {
        this.defaultProp = num;
    }
}

class MyClass extends DefaultClass {
    myProp ?: string;
    constructor(num: number, str ?: string) {
        super(num);
        this.myProp = str || "default value";
    }
    myMethod(str: string): string[] {
        return [str];
    }
}

interface Config {
    class: typeof DefaultClass;
}

const config: Config = {
    class: DefaultClass
}

function initConfig(classType: typeof DefaultClass) {
    config.class = classType;
}

// we don't care for the param type, it's irrelevant here
function myFunction(param: any): InstanceType<typeof config.class> {
    // does something based on which class is used in config
    // returns a DefaultClass or MyClass instance based on the current config
    return new config.class(1);
}

initConfig(MyClass);
const myInstance: MyClass = myFunction("something");
// ERR !: Property 'myMethod' is missing in type 'DefaultClass'
// but required in type 'MyClass'.

正如您猜测的那样,静态类型检查无法根据对 config 对象所做的更改动态更改返回类型,因为它在运行之前是“未知的”。但我想找到一种方法(如果有的话)来做到这一点。

1 个答案:

答案 0 :(得分:1)

直接在 TypeScript 中支持你正在做的事情的一个大问题是它需要:

  • 随时间变化的类型变量,以及
  • 这些效果需要跨功能边界可见。

无任意类型突变

TypeScript 不支持任意改变表达式或变量的类型。没有办法做类似 let foo = {a: "hello"}; foo = {b: 123}; 的事情并且让编译器将 foo 的明显类型从 {a: string} 更改为 {b: number}。您可以像 foo 这样注释 let foo: {a?: string; b?: number} 的类型,但是编译器不会注意到从一个赋值到下一个赋值发生了任何变化。

所做的只有narrowing via control flow analysis。因此,如果您有某个已知类型的值,则可以通过赋值或其他工具(如 user-defined type guardsassertion functions)使变量的表观类型更加具体。因此,您可以想象地让编译器注意到 config 已从类型 {class: typeof Defaultclass} 缩小到 {class: typeof MyClass}。但是:

跨函数边界没有类型缩小效果

它对您没有帮助,因为您希望 config 的调用者可以看到 myFunction() 类型的更改。这不会发生,因为 myFunction() 只会看到 config 类型直接发生在 myFunction() 的主体内部的变化。控制流缩小不会跨越函数边界持续存在,因为不可能实现既有效又不会严重降低编译器性能的通用解决方案。有一个 GitHub 问题,microsoft/TypeScript#9998,讨论了在调用函数时尝试处理缩小效应的问题。

所以你有点卡住了。


我的建议不是这样做,而是:如果您希望 TypeScript 帮助您,请做它理解的事情。对变量做的最好的事情就是在它的整个生命周期中保持相同的类型。您的代码的含义是永远不要通过调用 config 来修改 initConfig(),而是使用 initConfig 的参数来create myFunction()。这将不得不被打包成一种工厂,吐出一个实现。我们可以使用 class

class Impl<T extends typeof DefaultClass = typeof DefaultClass> {
    class: T
    constructor(classType: T = DefaultClass as T) {
        this.class = classType;
    }
    myFunction(param: any) {
        return new this.class(1) as InstanceType<T>;
    }
}

const CurImpl = new Impl(MyClass);
const myInstance: MyClass = CurImpl.myFunction("something");

const DifferentImpl = new Impl();
const differentInstance: DefaultClass = DifferentImpl.myFunction("else");

这里 CurImpl 知道 MyClass 因为它是 Impl<typeof MyClass> 的一个实例,并且它的类型永远不会改变。如果您想使用不同的类构造函数,您必须为某些 Impl<T> 创建一个新的 T 实例。


在上面,顺便说一句,我使用了 generic parameter default,因此如果无法推断出 T,它将使用 typeof DefaultClass。如果您不传入 classType 参数,编译器将使用 DefaultClass。这在技术上不是类型安全的,因为如果您不传入参数,编译器无法确定 T 实际上是 typeof DefaultClass。有人可以调用 new Impl<typeof MyClass>() 并破坏事物。假设没有人会做这样的事情,我使用 type assertion 只是告诉编译器,如果 classType 没有传入,那么 DefaultClass 值可以分配给类型 {{1} }.可能有更多类型安全的方法来做到这一点(可能没有 T),但我不想像所问的那样离题太远。

Playground link to code