Випуск із загальними властивостями під час відображення типу


11

У мене є бібліотека, яка експортує тип утиліти, подібний до наступного:

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

Цей тип утиліти дозволяє оголосити функцію, яка буде виконуватись як "дія". Він отримує загальний аргумент - це Modelте, що дія буде діяти проти.

dataАргумент «дії», потім набирається з іншим типом корисності цього експорту I;

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

Це цікава проблема. Типовий сценарій не може зробити багато що стосується загальних параметрів типу в умовних типах. Він просто відкладає будь-яку оцінкуextends якщо виявить, що оцінка включає параметр типу.

Виняток застосовується, якщо ми можемо отримати typecript використовувати спеціальний тип відношення типу, а саме відношення рівності (не відношення розширення). Співвідношення рівності є зрозумілим для компілятора, тому немає необхідності відкладати оцінку умовного типу. Загальні обмеження - одне з небагатьох місць у компіляторі, де використовується рівність типів. Розглянемо приклад:

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

Посилання на дитячий майданчик

Ми можемо скористатися такою поведінкою для виявлення конкретних типів. Тепер це буде точне відповідність типу, а не збільшене, а точні відповідність не завжди підходить. Однак, оскільки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;
  // }

}

Посилання на дитячий майданчик

Вищеописаний тип 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;
  // }

}

Посилання на дитячий майданчик

Остаточне рішення виглядає приблизно так:

// 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;
    }
  };
}

Посилання на дитячий майданчик

ПРИМІТКИ. Тут обмеження полягає в тому, що це працює лише з одним параметром типу (хоча він, можливо, може бути адаптований до більше). Крім того, API трохи заплутаний для будь-яких споживачів, тому це може бути не найкращим рішенням. Можуть бути проблеми, які я ще не визначив. Якщо ви знайдете будь-яке, повідомте мене 😊


2
Я відчуваю, що Гандальф Білий щойно відкрив себе. 🤯 TBH Я був готовий списати це як обмеження компілятора. Тож, спробувавши це спробувати. Дякую! 🙇
ctrlplusb

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

Я мав намір застосувати щедрість до цієї відповіді, але у мене сильно не вистачає сну, дитячий мозок триває і неправильно клацає. Мої вибачення! Це фантастично прониклива відповідь. Хоча і досить складного характеру. 😅 Дуже дякую, що знайшли час, щоб відповісти на це.
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
}

Не ідеально, але чудово знати про напівпроблему :)
ctrlplusb

1

countі valueзавжди зробить компілятора нещасним. Щоб виправити це, ви можете спробувати щось подібне:

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

Оскільки Partialвикористовується тип утиліти, вам буде нормально, якщо transformметод відсутній.

Stackblitz


1
"count and value завжди зробить компілятора нещасним" - я би вдячний деяким розумінням того, чому тут. 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
    },
  };
}

Не впевнений, чи це те, що ви хотіли через досить складність, яку ви надали в додаткових типах утиліт. Сподіваюся, це допомагає.


Дякую, так, я хочу. Але це тип утиліти, яку я експортую для споживання сторонніх виробників. Я не знаю форми / властивостей їх об'єктів. Я просто знаю, що мені потрібно позбавити всіх функціональних властивостей і використовувати результат проти аргументу даних перетворення функцій.
ctrlplusb

Я оновив опис своєї проблеми, сподіваючись, що це стане більш зрозумілим.
ctrlplusb

2
Основне питання полягає в тому, що Т може бути також типом Дії, оскільки не визначено для його виключення. Надія знайде рішення. Але я в тому місці, де підрахунок нормальний, але T все ще опущено, оскільки це перетин з Дією
Мацей Сікора

Було б чудово, якби я міг виразити, що T не типу Action. Сорт зворотного розширення.
ctrlplusb

Відносне обговорення: stackoverflow.com/questions/39328700 / ...
ctrlplusb
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.