Проблема с общими свойствами при отображении типов


11

У меня есть библиотека, которая экспортирует тип утилиты, подобный следующему:

type Action<Model extends object> = (data: State<Model>) => State<Model>;

Этот тип утилиты позволяет вам объявить функцию, которая будет выполнять «действие». Он получает общий аргумент, против Modelкоторого будет действовать действие.

Затем dataаргумент «действия» набирается с помощью другого типа утилиты, который я экспортирую;

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

Тип Stateутилиты в основном принимает входящий Modelуниверсальный тип, а затем создает новый тип, в котором все свойства данного типа Actionбыли удалены.

Например, вот базовая пользовательская реализация земли выше;

interface MyModel {
  counter: number;
  increment: Action<Model>;
}

const myModel = {
  counter: 0,
  increment: (data) => {
    data.counter; // Exists and typed as `number`
    data.increment; // Does not exist, as stripped off by State utility 
    return data;
  }
}

Выше работает очень хорошо. 👍

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

Например;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

В приведенном выше примере я ожидаю, что dataаргумент будет напечатан там, где doSomethingдействие было удалено, а универсальное valueсвойство все еще существует. Это, однако, не так - valueсобственность также была удалена нашей Stateутилитой.

Я полагаю, что причина этого заключается в том, что он Tявляется общим без каких-либо ограничений / сужений типов, и поэтому система типов решает, что он пересекается с Actionтипом, а затем удаляет его из dataтипа аргумента.

Есть ли способ обойти это ограничение? Я сделал некоторые исследования и надеется , что будет какой - то механизм , в котором я мог бы утверждать , что Tлюбая , кроме для Action. т.е. отрицательное ограничение типа.

Представить:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {

Но эта функция не существует для TypeScript.

Кто-нибудь знает, как я могу заставить это работать так, как я ожидаю?


Чтобы помочь отладке, вот полный фрагмент кода:

// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Вы можете поиграть с этим примером кода здесь: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

Ответы:


7

Это интересная проблема. Typescript, как правило, мало что может сделать в отношении параметров универсального типа в условных типах. Он просто откладывает любую оценку, extendsесли обнаружит, что оценка включает параметр типа.

Исключение применяется, если мы можем заставить машинописный текст использовать специальный тип отношения типа, а именно, отношение равенства (не отношение расширяемых). Отношение равенства легко понять для компилятора, поэтому нет необходимости откладывать оценку условного типа. Общие ограничения являются одним из немногих мест в компиляторе, где используется равенство типов. Давайте посмотрим на пример:

function m<T, K>() {
  type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"

  // Generic type constrains are compared using type equality, so this can be resolved inside the function 
  type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"

  // If the types are not equal it is still un-resolvable, as K may still be the same as T
  type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO" 
}

Playground Link

Мы можем воспользоваться этим поведением для идентификации конкретных типов. Теперь это будет точное совпадение типов, а не совпадение по расширению, и точные совпадения типов не всегда подходят. Однако, поскольку Actionэто просто сигнатура функции, точные совпадения типов могут работать достаточно хорошо.

Давайте посмотрим, сможем ли мы извлечь типы, которые соответствуют более простой сигнатуре функции, такой как (v: T) => void:

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]: Identical<M[K], (v: T) => void, never, K>
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: Identical<T, (v: T) => void, never, "value">;
  //     other: "other";
  //     action: never;
  // }

}

Playground Link

Приведенный выше тип KeysOfIdenticalTypeблизок к тому, что нам нужно для фильтрации. Для other, имя свойства сохраняется. Для action, имя свойства стирается. Вокруг всего одна неприятная проблема value. Так как valueимеет тип T, это не тривиально разрешимо T, и (v: T) => voidне являются идентичными (и на самом деле они могут не быть).

Мы все еще можем определить, что valueидентично T: для свойств типа Tпересекаем эту проверку (v: T) => voidс never. Любое пересечение с neverтривиально разрешимо never. Затем мы можем добавить обратно свойства типа, Tиспользуя другую проверку идентичности:

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]:
      (Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
      | Identical<M[K], T, K, never> // add back any properties of type T
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: "value";
  //     other: "other";
  //     action: never;
  // }

}

Playground Link

Окончательное решение выглядит примерно так:

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
    [P in keyof Model]:
      (Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
    | Identical<Model[P], G, P, never>
  }[keyof Model]>;

// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;


type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

interface MyModel<T> {
  value: T; // 👈 a generic property
  str: string;
  doSomething: Action<MyModel<T>, T>;
  method() : void
}


function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    str: "",
    method() {

    },
    doSomething: data => {
      data.value; // ok
      data.str //ok
      data.method() // ok 
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

/// Still works for simple types
interface MyModelSimple {
  value: string; 
  str: string;
  doSomething: Action<MyModelSimple>;
}


function modelFactory2(value: string): MyModelSimple {
  return {
    value,
    str: "",
    doSomething: data => {
      data.value; // Ok
      data.str
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Playground Link

ЗАМЕЧАНИЯ: ограничение здесь состоит в том, что это работает только с одним параметром типа (хотя его можно адаптировать к большему количеству). Кроме того, API немного сбивает с толку любого потребителя, поэтому это может быть не лучшим решением. Могут быть проблемы, которые я еще не определил. Если вы найдете, дайте мне знать 😊


2
Я чувствую, что Гэндальф Белый просто проявил себя. 🤯 TBH Я был готов списать это как ограничение компилятора. Так рад, чтобы попробовать это. Спасибо! 🙇
ctrlplusb

@ctrlplusb 😂 LOL, этот комментарий сделал мой день 😊
Тициан Черникова-Драгомир

Я хотел применить награду к этому ответу, но у меня острая нехватка сна, и мозг ребенка продолжал ошибаться. Мои извинения! Это фантастически проницательный ответ. Хотя и довольно сложный по своей природе. So Большое спасибо, что нашли время ответить на него.
ctrlplusb

@ctrlplusb :( Ну да ладно ... выиграй, проиграешь :)
Тициан Черникова-Драгомир

2

Было бы здорово, если бы я мог выразить, что T не относится к типу Action. Вроде инверсия расширяет

Точно так же, как вы сказали, проблема в том, что у нас пока нет негативных ограничений. Я также надеюсь, что они скоро смогут найти такую ​​возможность. В ожидании я предлагаю обходной путь, подобный этому:

type KeysOfNonType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];

// CHANGE: use `Pick` instead of `Omit` here.
type State<Model extends object> = Pick<Model, KeysOfNonType<Model, Action<any>>>;

type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T;
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Now it does exist 😉
      data.doSomething; // Does not exist 👍
      return data;
    }
  } as MyModel<any>; // <-- Magic!
                     // since `T` has yet to be known
                     // it literally can be anything
}

Не идеально, но приятно знать об
обходном

1

countи valueвсегда сделает компилятор несчастным. Чтобы исправить это, вы можете попробовать что-то вроде этого:

{
  value,
  count: 1,
  transform: (data: Partial<Thing<T>>) => {
   ...
  }
}

Так как Partialтип утилиты используется, вы будете в порядке, если transformметод отсутствует.

Stackblitz


1
«количество и значение всегда будут приводить к тому, что компилятор будет несчастлив», - я был бы признателен за понимание того, почему здесь. xx
ctrlplusb

1

Обычно я читаю это дважды и не до конца понимаю, чего вы хотите достичь. Насколько я понимаю, вы хотите исключить transformиз типа, который дан именно transform. Для этого достаточно использовать Omit :

interface Thing<T> {
  value: T; 
  count: number;
  transform: (data: Omit<Thing<T>, 'transform'>) => void; // here the argument type is Thing without transform
}

// 👇 the factory function accepting the generic
function makeThing<T>(value: T): Thing<T> {
  return {
    value,
    count: 1,
      transform: data => {
        data.count; // exist
        data.value; // exist
    },
  };
}

Не уверен, что это именно то, что вы хотели из-за сложности, которую вы указали в дополнительных типах утилит. Надеюсь, поможет.


Спасибо, да, я хочу. Но это тип утилиты, которую я экспортирую для стороннего потребления. Я не знаю форму / свойства их объектов. Я просто знаю, что мне нужно удалить все свойства функции и использовать результат против аргумента данных transform func.
ctrlplusb

Я обновил свое описание проблемы в надежде, что оно станет более понятным.
ctrlplusb

2
Основная проблема заключается в том, что T также может быть типом Action, поскольку он не определен для его исключения. Надеюсь найду решение. Но я нахожусь в месте, где счет в порядке, но T по-прежнему опущен, потому что это пересечение с действием
Maciej Sikora

Было бы здорово, если бы я мог выразить, что T не относится к типу Action. Вроде инверсия расширяется.
ctrlplusb

Относительная дискуссия: stackoverflow.com/questions/39328700/…
ctrlplusb
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.