Определение типа машинописного текста неточно – почему?

В некоторых случаях компилятор машинописного текста не определяет правильные типы, что требует от меня избыточности. В моей голове заявлено, что эти типы абсолютно безопасны. Я что-то упустил или компилятор неправильный? И могу ли я как-нибудь помочь ему автоматически определить правильные типы, не повторяясь?

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}`);
        }
    }
}

Ссылка на Детская площадка

new Model().parse(Math.random() < 0.99 ? "value1" : "value2", "oops") показывает проблему. Чтобы исправить это, вероятно, нужно забыть об дженериках и использовать деструктурированный кортеж, как показано в этой ссылке на игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ или найду подходящий источник дублирования. Если нет, то что мне не хватает?
jcalz 04.06.2024 14:22

Компилятор прав. Вы можете вызвать 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; }

Bergi 04.06.2024 14:28

@jcalz это идеальное решение! Пожалуйста, опубликуйте как ответ <3

Verim 05.06.2024 10:28
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
3
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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 { }

и все по-прежнему работает.

Детская площадка, ссылка на код

Другие вопросы по теме