Выведите тип, чтобы несколько свойств были одинаковыми

#typescript

Вопрос:

Возможно ли в Typescript вывести несколько свойств одного и того же типа или, возможно, вывести типы на основе определенного свойства..

Допустим, у нас есть ->

 interface Test<T> {
  value1: T;
  value2: T;
}

const test1:Test<string> = {
    value1: 'string1',
    value2: 'string1'
}
 

В приведенном выше примере, если бы я превратил значение 1 или значение 2 в число, это было бы ошибкой, отлично. Но было бы неплохо удалить универсальный <string> , и чтобы он работал, он основан, возможно, на первых типах свойств value1 .

Сначала я подумал, что мог бы использовать значение по умолчанию для T как любое, и позже он определит тип, но он просто сохраняется как любой, как видно ниже, у нас есть строка и число.

 interface Test<T = any> {
  value1: T;
  value2: T;
}

const test1:Test = {
    value1: 'string1',
    value2: 2
}
 

Комментарии:

1. К сожалению, это очень сложно из-за того, что машинопись выводит типы союзов, поэтому она сделает вывод string | number , чтобы это сработало.

Ответ №1:

Как вы видели, параметр универсального типа по умолчанию не оказывает желаемого эффекта. Если вы ссылаетесь на тип как Test без указания параметра типа как в Test<string> , компилятор будет использовать значение по умолчанию и интерпретировать его как Test<any> . Это не будет для вас выводом string . Существует запрос функции, microsoft/TypeScript#10671, запрашивающий способ, чтобы компилятор выводил неуказанные параметры типа… что-то вроде Test<infer> или Test<*> . Но до сих пор TypeScript не обладает этой функцией.

Вместо этого вы можете создать универсальную служебную функцию, которая возвращает свои входные данные:

 const asTest = <T,>(test: Test<T>) => test;
 

Когда вы вызываете универсальную функцию, TypeScript попытается определить соответствующий тип для тестового параметра. Вы можете вызвать эту функцию вместо аннотирования переменной:

 const testString = asTest({
  value1: "string1",
  value2: "string2"
})
// const testString: Test<string>

const testStringNumber = asTest({
  value1: "string1",
  value2: 2 // error! 'number' is not assignable to 'string'
})
// const testString: Test<string>
 

Это ведет себя так, как хотелось бы.


Однако обратите внимание, что не всегда очевидно, будет ли компилятор отклонять значение или синтезировать тип объединения для своего параметра типа. В testStringNumber этом было бы разумно T сделать вывод как string | number , но эвристика, которую использует компилятор, заключается в том, что люди обычно не хотят выводить объединения примитивных типов.

С другой стороны, следующий код приводит к выводу объединения:

 const testUnion = asTest({
  value1: { a: "" },
  value2: { b: "" }
})
/* Test<{
    a: string;
    b?: undefined;
} | {
    b: string;
    a?: undefined;
}> */
 

Этот тип Test<{a: string; b?: undefined} | {b: string; a?: undefined}> точно описывает тип передаваемого значения asTest() . Эвристика компилятора предполагает, что объединения типов объектов являются более приемлемыми и ожидаемыми.


Но, возможно, вы предпочли бы что-то, что жалуется на такие союзы и использует только (скажем) value1 для вывода T , а затем просто проверяет value2 это. То есть вы хотели бы T , чтобы в value2 качестве параметра типа без вывода использовался параметр, как указано в microsoft/TypeScript#14829. Официальной прямой поддержки для этого нет, но есть несколько разумных способов смоделировать это. Например:

 const asTest = <T, U extends T>(test: { value1: T, value2: U }): Test<T> => test;
 

В этой версии asTest используются два параметра универсального типа: T для value1 и U для value2 . U ограничен возможностью назначения T , поэтому компилятор будет принимать входные данные только там, где value2 есть тип, которому можно назначить value1 . И компилятор сделает вывод, только T посмотрев value1 , а не на value2 . Это работает одинаково для testString и testStringNumber :

 const testString = asTest({ value1: "string1", value2: "string2" });
// const testString: Test<string>

const testStringNumber = asTest({
  value1: "string1",
  value2: 2 // error! 'number' is not assignable to 'string'
})
// const testString: Test<string>
 

Но теперь testUnion выдает ошибку:

 const testUnion = asTest({
  value1: { a: "" },
  value2: { b: "" } // error! '{ b: string; }' is not assignable to '{ a: string; }'
})
 

потому {b: ""} что не может быть присвоен типу {a: string} , из которого сделан вывод value1 .


Ссылка на игровую площадку для кода

Комментарии:

1. Спасибо, я думал, что схожу с ума, каждая моя попытка казалась мне провалом в кроличью нору. Я попробовал U extends T непосредственно на типе, но у этого была проблема с курицей и яйцом 🙂