Могу ли я использовать тип в качестве значения (или правильно вывести общий тип класса из параметра конструктора)? - PullRequest
1 голос
/ 08 мая 2019

У меня есть универсальный класс, который принимает другой тип и имя ключа из этого типа.Я хочу также инициализировать свойство с этим именем ключа.Я определил это в 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 с этим кодом: ссылка

Ответы [ 2 ]

2 голосов
/ 08 мая 2019

Плохо то, что вы не можете сделать это легко. Лучший способ сделать это - позволить компилятору выводить ExactKey на основе аргументов конструктора. Это может быть сделано только в том случае, если мы позволим машинописи выводить все параметры типа. Но вам нужно указать один параметр типа и вывести другой, и это невозможно, так как нет поддержки частичного вывода (по крайней мере, начиная с 3.5, планировалось добавить его, но он был отодвинут, а затем удален из дорожная карта).

Один из вариантов - использовать функцию curry со статической функцией вместо конструктора:

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

Ни одно из решений не является хорошим, ни одно из них не является особенно интуитивным, частичный вывод аргумента будет лучшим решением.

2 голосов
/ 08 мая 2019

Проблема в том, что вы хотите вручную указать один параметр типа, а компилятор выводит другой. Это называется вывод аргумента частичного типа , и у 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, что достаточно для того, чтобы тип выводился так, как вы хотите.

Другой обходной путь - разбить все, что обычно использует частичный вывод, на две части с помощью curry ; первая универсальная функция позволит вам указать один параметр, и она возвращает универсальную функцию (или универсальный конструктор в данном случае), которая выводит другой параметр. Например

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

Во всяком случае, надеюсь, что одна из этих работ для вас. Удачи!

...