Сопоставленные типы и ошибка типичной функции - PullRequest
0 голосов
/ 30 мая 2018

Я написал довольно простой код на основе отображаемых типов, который по какой-то причине не хочет проверять тип.

Сначала определим ввод и вывод:

interface Validated<T> {
  valid: boolean;
  value: T;
} 

interface FieldInputs {
  name: string;
  price: number;
}

interface ParsedFields {
  name: Validated<string>;
  price: Validated<number>;
}

Определим парсертипы и карта синтаксического анализатора:

type FieldKey = keyof FieldInputs & keyof ParsedFields;
type FieldParser<F extends FieldKey> = (value?: FieldInputs[F]) => ParsedFields[F];
type FieldParsers = {
  [F in FieldKey]: FieldParser<F>;
};

declare let fieldParsers: FieldParsers;

Теперь эта очень простая универсальная функция не проходит проверку типов:

function update<F extends FieldKey>(field: F, value: FieldInputs[F]) {
  const parser: FieldParser<F> = fieldParsers[field];
  parser.apply(value);
}

выдает следующую ошибку (--strictFunctionTypes):

Type 'FieldParsers[F]' is not assignable to type 'FieldParser<F>'.
  Type 'FieldParser<"name"> | FieldParser<"price">' is not assignable to type 'FieldParser<F>'.
    Type 'FieldParser<"name">' is not assignable to type 'FieldParser<F>'.
      Types of parameters 'value' and 'value' are incompatible.
        Type 'FieldInputs[F]' is not assignable to type 'string'.
          Type 'string | number' is not assignable to type 'string'.
            Type 'number' is not assignable to type 'string'.

Чего мне не хватает?

Playground Link

Ответы [ 2 ]

0 голосов
/ 30 мая 2018

Я думаю, вы, возможно, немного переборщили с типами.Упрощение их действительно решит вашу проблему.

Давайте начнем с рассмотрения определения вашего типа для FieldParser:

type FieldParser<F extends FieldKey> = (value?: FieldInputs[F]) => ParsedFields[F];

Все, что он действительно делает, это принимает значение и возвращает Validated объект того же типа.Мы можем упростить это до:

type FieldParser<T> = (value?: T) => Validated<T>;

Это не только повышает сложность, но и значительно улучшает читаемость типа.

Обратите внимание, однако, что это означает, что мы утратили ограничение на FieldParser, что оно может использоваться только с ключами от FieldKey.Но на самом деле, если вы думаете об общей концепции «анализатора поля», она должна быть общей, и, как мы увидим через секунду, это не означает, что ваш код потребления становится менее строгим.

Мы также можем затем построить FieldParsers как универсальный тип

type FieldParsers<T> = {
    [K in keyof T]: FieldParser<K>;
}

Тогда оставшаяся часть кода может использовать те без проблем:

interface MyFieldInputs {
  name: string;
  price: number;
}

declare let fieldParsers: FieldParsers<MyFieldInputs>;

function update<T extends keyof MyFieldInputs>(field: T, value: MyFieldInputs[T]) {
  const parser = fieldParsers[field];
  parser.apply(value);
}

Однако мы можем сделать еще лучше,Вам все еще нужно использовать parser.apply(value) здесь, когда на самом деле вы сможете просто вызвать parser(value).

Давайте сделаем дженерики еще на один шаг вперед, а не будем жестко кодировать функцию update, чтобы использоватьконкретная переменная fieldParsers, которую мы определили перед функцией, давайте использовать функцию для построения функции обновления.

function buildUpdate<TInputs>(parsers: FieldParsers<TInputs>) {
  return function update<T extends keyof TInputs>(field: T, value: TInputs[T]) {
    const parser = parsers[field];
    parser(value);
  }
}

Таким образом, мы можем легко связать все типы, и Typescript просто примет(и проверка типов) вызов parser(value).

Итак, собрав все это вместе, вы получите:

interface Validated<T> {
  valid: boolean;
  value: T;
} 

/**
 * Generic field validator
 */
type FieldParser<T> = (value?: T) => Validated<T>;

/**
 * Generic set of field validators for a specific set of field types
 */
type FieldParsers<T> = {
  [K in keyof T]: FieldParser<T[K]> 
}

function buildUpdate<TInputs>(parsers: FieldParsers<TInputs>) {
  return function update<T extends keyof TInputs>(field: T, value: TInputs[T]) {
    const parser = parsers[field];
    parser(value);
  }
}

И вы воспользуетесь им, выполнив:

interface MyFieldInputs {
  name: string;
  price: number;
}

declare let fieldParsers: FieldParsers<MyFieldInputs>;

const update = buildUpdate(fieldParsers);

update('name', 'new name'); // Fully type checked

update('name', 5); // ERROR
0 голосов
/ 30 мая 2018

Компилятор защищает вас от чего-то маловероятного, и вам нужно решить, как обойти это (предупреждение спойлера: используйте утверждение типа )


Представьте, еслиЯ делаю это:

const field = Math.random() < 0.5 ? "name" : "price";
const value = Math.random() < 0.5 ? "Widget" : 9.95;
update(field, value); // no error

В этом случае field относится к типу FieldKey, а value относится к типу FieldInputs[FieldKey], и существует 50% вероятность того, что они нене совпадают.Несмотря на это, компилятор не предупреждает вас: он выводит, что F - это FieldKey (что вполне допустимо), и вызов update() разрешен.

Внутри реализации update() есть предупреждение о том, что FieldParsers[F] может не быть FieldParser<F>.Если F равно FieldKey, как указано выше, это несоответствие становится очевидным.FieldParsers[F] будет FieldParser<'name'> | FieldParser<'price'>, но FieldParser<F> будет FieldParser<'name' | 'price'>.Первым является или что-то, что анализирует string или что-то, что анализирует number.Последний - то, что анализирует или a string или number.Они не одинаковы (из-за контравариантности параметров функции , включенной с --strictFunctionTypes).Разница между этими типами проявляется, когда приведенный выше код вызывает update("name", 9.95) и вы пытаетесь проанализировать number с помощью string синтаксического анализатора.Вы хотите FieldParser<F>, но все, что у вас есть, это FieldParsers[F].


Теперь резервное копирование - это кто-то , вероятно, , чтобы играть в подобные игры, где F являетсясоюз ценностей?Если это так, то вы можете изменить определение update(), чтобы явно запретить F быть чем-то иным, кроме однострочного литерала.Что-то вроде ...

type NotAUnion<T> = [T] extends [infer U] ? 
  U extends any ? [T] extends [U] ? T : never : never : never;

declare function update<F extends FieldKey>(
  field: F & NotAUnion<F>, 
  value: FieldInputs[F]
);

Но это, вероятно, излишне, и все же не разрешает предупреждение внутри реализации update().Компилятор просто не достаточно умен, чтобы понять, что значение F представляет собой одностроковое литеральное значение и что то, что вы делаете, безопасно.

Чтобы устранить эту ошибку, вы, вероятно, захотите сделать утверждение типа .Либо вы знаете, что никто не может преднамеренно выстрелить себе в ногу, увеличив F до FieldKey, либо вы помешали звонящему сделать это, используя что-то вроде NotAUnion.В любом случае вы можете сообщить компилятору, что вы знаете, что fieldParsers[field] будет действительным FieldParser<F>:

function update<F extends FieldKey>(field, value: FieldInputs[F]) {
  const parser = fieldParsers[field] as FieldParser<F>; // okay
  parser.apply(value);
}

Так что это работает.Надеюсь, это поможет.Удачи!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...