我可以使用类型作为值(或从构造函数参数正确推断通用类类型)吗?

时间:2019-05-08 15:00:12

标签: typescript typescript-generics

我有一个通用类,该类接受其他类型和该类型的键名。我还想用此键名初始化属性。我已经在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

2 个答案:

答案 0 :(得分:2)

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

有两种主要的解决方法:

首先是完全依靠类型推断,并使用 dummy参数来推断通常要指定的类型。例如:

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">

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

答案 1 :(得分:2)

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

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

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')

另一种选择是在构造函数args中为编译器提供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 将是最佳解决方案。