【问题标题】:Can I use type as value (or correctly infer generic class type from constructor parameter)?我可以将类型用作值(或从构造函数参数正确推断泛型类类型)吗?
【发布时间】:2019-09-26 08:36:22
【问题描述】:

我有一个泛型类,它接受另一种类型和这种类型的键名。我还想用这个键名初始化属性。我已经在 TS 中定义了这个,但我必须明确地将键的名称作为通用参数传递,并将相同的值传递给构造函数。它看起来有点不必要 - 这些值总是相同的(类型和值)。

是否可以从其构造函数参数推断泛型类的类型?或者使用通用“类型”作为“值”(可能不是 - 我有错误:'ValueKey' only refers to a type, but is being used as a value here.ts(2693))。

带定义的代码:

interface InterfaceWithProperties {
    stringProperty: string;
    numberProperty: number;
}

class GenericCLass<TypeWithKeys, ExactKey extends keyof TypeWithKeys> {
    keyNameFromAnotherType: ExactKey; // this property should have value equal to key from another type;

    // is it a way to remove this parameter? it's exactly duplicated with ExactKey
    // or to detect correctly ExactKey type based on `property` value
    constructor(keyNameFromAnotherType: ExactKey) {
        // this.property = ExactKey; // ofc: not working code
        this.keyNameFromAnotherType = keyNameFromAnotherType;
    }
}

当前使用情况:

const test1 = new GenericCLass<InterfaceWithProperties, 'stringProperty'>('stringProperty');

我想要相同的结果,但没有这个额外的参数。像这样的:

const test2 = new GenericCLass<InterfaceWithProperties, 'stringProperty'>();

或者像这样:

const test3 = new GenericCLass<InterfaceWithProperties>('stringProperty');

带有此代码的 TS 游乐场:link

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    坏消息是你不能轻易做到这一点。最好的方法是让编译器根据构造函数参数推断ExactKey。只有当我们让 typescript 推断所有类型参数时,才能做到这一点。但是您需要指定一个类型参数并推断另一个,这是不可能的,因为不支持部分推断(至少从 3.5 开始,有计划添加它,但它被推回,然后从路线图)。

    一种选择是使用带有静态函数而不是构造函数的函数柯里化:

    interface InterfaceWithProperties {
        stringProperty: string;
        numberProperty: number;
    }
    
    class GenericCLass<TypeWithKeys, ExactKey extends keyof TypeWithKeys> {
        keyNameFromAnotherType: ExactKey; 
        private constructor(keyNameFromAnotherType: ExactKey) {
            this.keyNameFromAnotherType = keyNameFromAnotherType;
        }
        static new<T>(){
            return function <ExactKey extends keyof T>(keyNameFromAnotherType: ExactKey) {
                return new GenericCLass<T, ExactKey>(keyNameFromAnotherType);
            }         
        } 
    }
    
    
    const test1 = GenericCLass.new<InterfaceWithProperties>()('stringProperty')
    

    另一种选择是在构造函数参数中为编译器提供TypeWithKeys 的推理站点:

    interface InterfaceWithProperties {
        stringProperty: string;
        numberProperty: number;
    }
    
    class GenericCLass<TypeWithKeys, ExactKey extends keyof TypeWithKeys> {
        keyNameFromAnotherType: ExactKey; 
        constructor(t: TypeWithKeys, keyNameFromAnotherType: ExactKey) {
            this.keyNameFromAnotherType = keyNameFromAnotherType;
        }
    }
    
    
    const test1 = new GenericCLass(null as InterfaceWithProperties, 'stringProperty')
    

    或者如果null as InterfaceWithProperties 吓到你,你可以使用Type 类:

    class Type<T> { private t:T }
    class GenericCLass<TypeWithKeys, ExactKey extends keyof TypeWithKeys> {
        keyNameFromAnotherType: ExactKey; 
        constructor(t: Type<TypeWithKeys>, keyNameFromAnotherType: ExactKey) {
            this.keyNameFromAnotherType = keyNameFromAnotherType;
        }
    }
    
    
    const test1 = new GenericCLass(new Type<InterfaceWithProperties>(), 'stringProperty')
    

    这两种解决方案都不是很好,也不是特别直观,partial argument inference 将是最好的解决方案。

    【讨论】:

      【解决方案2】:

      问题是您想手动指定一个类型参数并让编译器推断另一个。这称为 partial type argument inference 而 TypeScript 没有它(从 TS3.4 开始)。您可以手动指定所有类型参数(您不想这样做),也可以让编译器推断出所有类型参数(您不能这样做,因为您无法从中推断出指定的类型)。

      对于这种情况有两种主要的解决方法:

      第一个是完全依赖类型推断并使用一个虚拟参数来推断你通常会指定的类型。例如:

      class GenericCLass<T, K extends keyof T> {
          keyNameFromAnotherType: K; 
      
          // add dummy parameter
          constructor(dummy: T, keyNameFromAnotherType: K) {
              this.keyNameFromAnotherType = keyNameFromAnotherType;
          }
      }
      
      const test = new GenericCLass(null! as InterfaceWithProperties, 'stringProperty');
      // inferred as GenericCLass<InterfaceWithProperties, "stringProperty">
      

      可以看到,作为第一个参数传入的值在运行时只是null,而构造函数无论如何在运行时都不看它。但是类型系统已被告知它是 InterfaceWithProperties 类型,这足以让您按照您想要的方式推断类型。

      另一种解决方法是通过currying 将通常使用部分推理的任何东西分成两部分;第一个泛型函数将让您指定一个参数,它返回一个推断另一个参数的泛型函数(或本例中的泛型构造函数)。例如

      // unchanged
      class GenericCLass<T, K extends keyof T> {
          keyNameFromAnotherType: K;
      
          constructor(keyNameFromAnotherType: K) {
              this.keyNameFromAnotherType = keyNameFromAnotherType;
          }
      }
      
      // curried helper function
      const genericClassMaker =
          <T>(): (new <K extends keyof T>(
              keyNameFromAnotherType: K
          ) => GenericCLass<T, K>) =>
              GenericCLass;
      
      // specify the one param
      const InterfaceWithPropertiesGenericClass =
          genericClassMaker<InterfaceWithProperties>();
      
      // infer the other param
      const test = new InterfaceWithPropertiesGenericClass('stringProperty');
      // inferred as GenericCLass<InterfaceWithProperties, "stringProperty">
      

      这会留下您的类定义,但会创建一个新的辅助函数,该函数会返回部分指定版本的 GenericClass 构造函数供您使用。你可以一次完成,但它很丑:

      const test = new (genericClassMaker<InterfaceWithProperties>())('stringProperty');
      // inferred as GenericCLass<InterfaceWithProperties, "stringProperty">
      

      无论如何,希望其中一个对你有用。祝你好运!

      【讨论】:

      • 伟大的思想都一样:) 我会留下我的答案,因为我认为Type 版本可能有一些用途,而你的函数返回构造函数方法也很好
      • 两个答案 (2nd answer) 都很棒(建议使用 Type 删除 null casting!我从 @jcalz 中使用了 curried helper function,因为我需要构造函数本身(而不是实例)。非常感谢 - 一切都像魅力一样 :)
      猜你喜欢
      • 2021-08-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-17
      • 1970-01-01
      • 2019-09-23
      • 2016-11-01
      • 1970-01-01
      相关资源
      最近更新 更多