В некоторых случаях компилятор машинописного текста не определяет правильные типы, что требует от меня избыточности. В моей голове заявлено, что эти типы абсолютно безопасны. Я что-то упустил или компилятор неправильный? И могу ли я как-нибудь помочь ему автоматически определить правильные типы, не повторяясь?
interface Dto {
value1: boolean;
value2: string;
value3: string;
}
class Model {
firstValue!: boolean;
secondValue!: number;
thirdValue!: string;
parse<
dtoName extends keyof Dto,
>(entry: dtoName, raw: Dto[dtoName]): void {
switch(entry) {
case 'value1':
/* raw is save a boolean isn't it? Its declared as …
Dto[dtoName]
= Dto['value1']
= boolean
*/
this.firstValue = raw;
break;
case 'value2':
this.secondValue = Number(raw);
break;
case 'value3':
this.thirdValue = raw; // same here
break;
default:
throw new Error(`Unknown entry ${entry}`);
}
}
}
Ссылка на Детская площадка
Компилятор прав. Вы можете вызвать model.parse<'value1' | 'value2'>('value1', 'nope'). На самом деле вам нужно перегрузить parse, чтобы быть { (entry: 'value1', raw: string): void; raw: boolean): void; (entry: 'value2', raw: string): void; (entry: 'value3', raw: string): void; }
@jcalz это идеальное решение! Пожалуйста, опубликуйте как ответ <3






TypeScript в настоящее время не может использовать анализ потока управления , чтобы влиять на общие параметры типа, такие как K (изменено с dtoName, который является параметром типа с нетрадиционным именем; по крайней мере, это должно быть DtoName, но K еще более условен для параметр типа ключа). Поэтому, когда вы проверяете entry, TypeScript может сузить entry от K до, скажем, "value1", но сам K останется неизменным. А это значит, что raw типа Dto[K] тоже не меняется. Существуют различные запросы на открытые функции, например microsoft/TypeScript#33014, здесь просят что-то получше, но на данный момент это невозможно.
Одним из основных камней преткновения на пути реализации этого является то, что, как написано, K сам по себе может быть типом объединения . Это может быть не "value1", "value2" или "value3". Это также может быть, скажем, "value1" | "value2". А это значит, что Dto[K] может быть boolean | string. Таким образом, ничто не мешает вызову, подобному следующему:
new Model().parse(
Math.random() < 0.99 ? "value1" : "value2",
"oops"
);
Это принимается TypeScript, однако существует вероятность 99%, что вы получите входные данные, которые ваша реализация не ожидает. Таким образом, ошибка компилятора технически правильна; действительно верно, что entry может быть "value1", тогда как raw может иметь тип string вместо boolean. Таким образом, для улучшения языковой поддержки того типа общего кода, который вы пишете, потребуется какой-то способ сказать: «K не может быть объединением», например, запрос функции по адресу microsoft/TypeScript#27808. И опять же, на данный момент это не часть языка.
Так что вам придется поработать над этим. Либо вы отказываетесь от анализа потока управления (например, switch/case), либо отказываетесь от дженериков, либо используете что-то вроде утверждений типа и отказываетесь от безопасности типов, проверяемой компилятором.
В вашем случае вы действительно можете отказаться от дженериков, ничего не потеряв. Тип возвращаемого значения parse() — void и не зависит от входных данных. Это означает, что функция не обязательно должна быть универсальной. Вместо того, чтобы ваши параметры были общими, вы можете использовать функцию остального параметра типа кортежа, например:
parse(...[entry, raw]:
[entry: "value1", raw: boolean] |
[entry: "value2", raw: string] |
[entry: "value3", raw: string]
): void {
switch (entry) {
case 'value1':
this.firstValue = raw; // okay
break;
case 'value2':
this.secondValue = Number(raw);
break;
case 'value3':
this.thirdValue = raw; // okay
break;
default:
throw new Error(`Unknown entry ${entry}`);
}
}
Этот союз кортежей представляет собой дискриминируемый союз , где первый элемент можно использовать в качестве дискриминанта для сужения типа второго элемента. Переменные entry и raw деструктурированы из оставшегося параметра, а TypeScript поддерживает анализ потока управления для деструктурированных дискриминируемых объединений. Может показаться странным, что функция принимает (...[entry, raw]) вместо (entry, raw), но это эквивалентно, и этот подход имеет то преимущество, что действительно работает на вас. Итак, теперь, когда вы проверяете entry, TypeScript может соответствующим образом сузить raw.
И теперь проблемный вызов, который был раньше, запрещен:
new Model().parse(
Math.random() < 0.99 ? "value1" : "value2",
"oops"
); // error!
Потому что ["value1" | "value2", string] не соответствует ни одному из трёх членов союза.
Это ответ на заданный вопрос, хотя писать такое объединение кортежей самостоятельно утомительно и излишне. К счастью, вы можете вычислить это по Dto следующим образом:
type Args = { [K in keyof Dto]: [entry: K, raw: Dto[K]] }[keyof Dto]
Это тип дистрибутивного объекта, придуманный в microsoft/TypeScript#47109 , который представляет собой отображаемый тип , в который вы немедленно индексируете, чтобы получить объединение свойств отображаемого типа. Если у вас есть функция типа F<K>, которую вы хотите распределить по объединениям в K, вы можете написать {[P in K]: F<P>}[K]. Таким образом, приведенное выше становится объединением [entry: K, raw: Dto[K]] для каждого K в keyof Dto.
Вооружившись этим, вы можете сделать параметр rest параметра parse() типа Args:
parse(...[entry, raw]: Args): void { }
и все по-прежнему работает.
new Model().parse(Math.random() < 0.99 ? "value1" : "value2", "oops")показывает проблему. Чтобы исправить это, вероятно, нужно забыть об дженериках и использовать деструктурированный кортеж, как показано в этой ссылке на игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ или найду подходящий источник дублирования. Если нет, то что мне не хватает?