Используйте обобщенные элементы для принудительного применения однотипных свойств в массиве интерфейса. - PullRequest
1 голос
/ 03 мая 2019

Я собираюсь использовать обобщенные элементы для обеспечения того, чтобы тип val1 соответствовал типу val2 для каждого элемента в массиве.

interface SameTypeContainer<T> {
  val1: T,
  val2: T;
}

test([
  {
    val1: 'string',
    val2: 'also string'
  },
  {
    val1: 5,
    val2: false // expect to throw error since type is not number
  }
]);

function test(_: SameTypeContainer<any>[]) { }

Это не вызывает ошибку.Я ожидаю, что это вызовет ошибку машинописи по следующей причине:

Во втором элементе массива, переданном в тестовую функцию, val1 - это число, а val2 - это строка.Интерфейс SameTypeContainer должен обеспечивать, чтобы тип val1 соответствовал типу val2.

Далее я попытался переопределить функцию теста для использования обобщений:

function test<T>(_: SameTypeContainer<T>[]) { }

Теперь я получаю сообщение об ошибке, но по неправильной причине.Компилятор ожидает, что val1 будет иметь тип string, а val2 будет иметь тип string, потому что именно так был определен первый элемент в массиве.

Я хочу, чтобы каждый элемент в массиве оценивалсянезависимо от того, удовлетворяет ли оно данным генерикам независимо.

Буду признателен за любую помощь!


ОБНОВЛЕНИЕ:

Спасибо за вашу помощь!Я ценю его!Я начинаю понимать использование extends, но у меня возникают проблемы с расширением его до моего фактического варианта использования:

export type Selector<S, Result> = (state: S) => Result;

export interface SelectorWithValue<S, Result> {
  selector: Selector<S, Result>;
  value: Result;
}

export interface Config<T, S, Result> {
  initialState?: T;
  selectorsWithValue?: SelectorWithValue<S, Result>[];
}

export function createStore<T = any, S = any, Result = any>(
  config: Config<T, S, Result> = {}
): Store<T, S, Result> {
  return new Store(config.initialState, config.selectorsWithValue);
}

export class Store<T, S, Result> {
  constructor(
    public initialState?: T,
    public selectorsWithValue?: SelectorWithValue<S, Result>[]
  ) {}
}

const selectBooleanFromString: Selector<string, boolean> = (str) => str === 'true';
const selectNumberFromBoolean: Selector<boolean, number> = (bool) => bool ? 1 : 0;

createStore({
  selectorsWithValue: [
    { selector: selectBooleanFromString, value: false },
    { selector: selectNumberFromBoolean, value: 'string' } // should error since isn't a number
  ],
});

Желаемый: для каждого элемента в массиве, переданного в функцию createStore, второй типselector должен соответствовать типу value.

Пример: если свойство selector имеет тип Selector<boolean, number>, свойство value должно иметь тип number, независимо от того, чтодругие элементы типов массива.

Игровая площадка Typescript

Вот моя первая попытка изменить игровую площадку Typescript @jcalz для указанного выше вложенного варианта использования:

Детская игровая площадка

Ответы [ 3 ]

0 голосов
/ 03 мая 2019

То, что происходит, заключается в том, что машинопись пытается лучше всего определить тип для вас, и поэтому она просто расширяет универсальный T до объединения строк | номер | логическое значение, так как это три возможных типа в массиве.

Что здесь следует печатать? это должно вывести это из val1? val2? число или логическое значение? первая ссылка? или последняя ссылка? Там действительно нет "правильного" ответа

Чтобы исправить это, вы можете сделать что-то вроде этого ..... хотя это не единственный способ. «Правильный путь» действительно зависит от вашей программы.

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true

interface SameTypeContainer<T> {
  val1: T,
  val2: T;
}

test([
  {
    val1: 'string',
    val2: 'also string'
  },
  {
    val1: "",
    val2: "false" // fine.
  }
]);

type PullTypeContainer<T extends SameTypeContainer<unknown>> =T extends SameTypeContainer<infer TEE> ? TEE : never

const test = <T extends SameTypeContainer<any>>(arg: (IsUnion<PullTypeContainer<T>> extends true ? "No unions" : T)[]) => {

}
0 голосов
/ 03 мая 2019

С тех пор как @jcalz поднял его, имейте некоторую экзистенциальную типизацию! Я уже опубликовал этот ответ , поэтому я сделаю этот CW. Другие ответы могут быть более идиоматическими (таким образом, лучше); но этот должен быть правильный , в том смысле, что он теоретически обоснован и поэтому должен уметь справляться с любыми хитростями.

У вас есть ваш параметрический тип:

interface SameTypeContainer<T> {
  val1: T,
  val2: T;
}

Существуют "универсальные SameTypeContainer потребители", которые имеют следующий универсально количественный тип (параметризованный по их типу возврата)

type SameTypeConsumer<R> = <T>(c: SameTypeContainer<T>) => R

Если у вас есть SameTypeContainer<T>, но вы не знаете, что такое T, единственное, что вы можете с ним сделать - это передать его в SameTypeConsumer<R>, который не заботится что такое T и получить обратно R (который не зависит от T). Итак, SameTypeContainer<T> -with-unknown- T эквивалентна функции, которая принимает любого потребителя, который не заботится о T, и запускает его сама:

type SameType = <R>(consumer: SameTypeConsumer<R>) => R
           // = <R>(consumer: <T>(sameType: SameTypeContainer<T>) => R) => R

Конечный продукт - это способность скрывать тип SameTypeContainer при закрытии анонимной функции. Итак, у нас есть тип и значение в зависимости от этого типа, хранящиеся в структуре данных, тип которой описывает только отношения между ними. Это зависимая пара; мы сделали!

function sameType<T>(c: SameTypeContainer<T>): SameType {
     return <R>(consumer: SameTypeConsumer<R>) => consumer(c)
}

«Закапывание» такого типа позволяет вам вводить SameTypeContainer всех различных типов в один большой тип объединения SameType, который вы можете использовать в качестве элементов массива в вашем случае.

let list: SameType[] = [ sameType({ val1: 'string', val2: 'also string' })
                       , sameType({ val1: 42, val2: 42 })
                       , sameType({ val1: {}, val2: {} })
                    // , sameType({ val1: 1, val2: false }) // error!
                       ]
function test(l: SameType[]): void {
  let doc = "<ol>"
  for(let s of l) {
    // notice the inversion
    let match = s(same => same.val1 === same.val2)
    doc += "<li>" + (match ? "Matches" : "Doesn't match") + "</li>"
  }
  doc += "</ol>"
  document.write(doc)
}
// it may be favorable to immediately destructure the pair as it comes into scope:
function test(l: SameType[]): void {
  let doc = "<ol>"
  for (let s0 of l) s0(s => {
    // this way, you can wrap the "backwardsness" all the way around your
    // code and push it to the edge, avoiding clutter.
    let match = s.val1 === s.val2 ? "Matches" : "Doesn't match"
    doc += "<li>" + match + "</li>"
  })
  doc += "</ol>"
  document.write(doc)
}

test(list)

%20%7B%0D%0A%20%20val1%3A%20T%2C%0D%0A%20%20val2%3A%20T%3B%0D%0A%7D%0D%0Atype%20SameTypeConsumer%20%3D%20(c%3A%20SameTypeContainer)%20%3D>%20R%0D%0Atype%20SameType%20%3D%20(consumer%3A%20SameTypeConsumer)%20%3D>%20R%0D%0A%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20%3D%20(consumer%3A%20(sameType%3A%20SameTypeContainer)%20%3D>%20R)%20%3D>%20R%0D%0Afunction%20sameType(c%3A%20SameTypeContainer)%3A%20SameType%20%7B%0D%0A%20%20%20%20%20return%20(consumer%3A%20SameTypeConsumer)%20%3D>%20consumer(c)%0D%0A%7D%0D%0Alet%20list%3A%20SameType%5B%5D%20%3D%20%5B%20sameType(%7B%20val1%3A%20'string'%2C%20val2%3A%20'also%20string'%20%7D)%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%20sameType(%7B%20val1%3A%2042%2C%20val2%3A%2042%20%7D)%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%20sameType(%7B%20val1%3A%20%7B%7D%2C%20val2%3A%20%7B%7D%20%7D)%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20%2C%20sameType(%7B%20val1%3A%201%2C%20val2%3A%20false%20%7D)%20%2F%2F%20error!%0D%0A%5D%0D%0A%2F%2F%20it%20may%20be%20favorable%20to%20immediately%20destructure%20the%20pair%20as%20it%20comes%20into%20scope%3A%0D%0Afunction%20test(l%3A%20SameType%5B%5D)%3A%20void%20%7B%0D%0A%20%20let%20doc%20%3D%20"

  1. "%20%2B%20match%20%2B%20"<%2Fli>"%0D%0A%20%20%7D)%0D%0A%20%20doc%20%2B%3D%20"<%2Fol>"%0D%0A%20%20document.write(doc)%0D%0A%7D%0D%0A%0D%0Atest(list)" rel="nofollow noreferrer"> Это должно вывести
:

  1. Не соответствует
  2. Соответствует
  3. Не соответствует

Вы можете найти полезным дальнейшее определение

function onSameType<R>(c: SameTypeConsumer<R>): (s: SameType) => R {
  return s => s(c)
}

Чтобы вы могли применять функции в направлении «вперед»:

function someFunction<T>(c: SameTypeContainer<T>): R
let s: SameType
s(someFunction) // "backwards"
let someFunction2 = onSameType(someFunction)
someFunction2(s) // "forwards"
0 голосов
/ 03 мая 2019

Причина, по которой Array<SameTypeContainer<any>> не работает, заключается в том, что буквально любое значение присваивается any, поэтому {val1: x, val2: y} будет иметь тип SameTypeContainer<any>, независимо от того, какие x и y.


Тип, который вы ищете - это массив, где каждый элемент имеет тип некоторый SameTypeContainer<T>, но не какой-либо конкретный T.Это, вероятно, лучше всего выражать как экзистенциальный тип , такой как (возможно) Array<SameTypeContainer<exists T>>, который в настоящее время не поддерживается изначально в TypeScript (как и большинство других языков с обобщениями).TypeScript (и большинство других языков с обобщениями) имеет только универсальные типы: кто-то, кто хочет значение типа X<T>, может указать любой тип для T, который он хочет, и поставщик значения должен бытьв состоянии подчиниться.Экзистенциальный тип противоположен: тот, кто хочет предоставить значение типа, например X<exists T>, может выбрать любой конкретный тип для T, который ему нужен, и получатель изэто значение просто должно соответствовать.Но в TypeScript нет экзистенциальных типов, поэтому нам придется заняться чем-то другим.

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


Следующее, что мы можем сделать, это использовать вывод универсального типа, разрешив test() быть универсальной функцией, принимающей параметр универсального типа A, который расширяет Array<SameContainer<any>>, а затем убедитесь, что A соответствует желаемому ограничению.Вот один из способов, которым мы можем это сделать:

interface SameTypeContainer<T> {
  val1: T;
  val2: T;
}

// IsSomeSameTypeContainerArray<A> will evaluate to A if it meets your constraint
// (it is an array where each element is a SameTypeContainer<T> for *some* T)
// Otherwise, if you find an element like {val1: T1, val2: T2} for two different 
// types T1, and T2, replace that element with the flipped version {val1: T2, val2: T1}    
type IsSomeSameTypeContainerArray<
  A extends Array<SameTypeContainer<any> >
> = {
  [I in keyof A]: A[I] extends { val1: infer T1; val2: infer T2 }
    ? { val1: T2; val2: T1 }
    : never
};

// test() is now generic in A extends Array<SameTypeContainer<any>>
// the union with [any] hints the compiler to infer a tuple type for A 
// _ is of type A & IsSomeSameTypeContainerArray<A>.  
// So A will be inferred as the type of the passed-in _,
// and then checked against A & IsSomeSameTypeContainerArray<A>.
// If it succeeds, that becomes A & A = A.
// If it fails on some element of type {val1: T1, val2: T2}, that element
// will be restricted to {val1: T1 & T2, val2: T1 & T2} and there will be an error
function test<A extends Array<SameTypeContainer<any>> | [any]>(
  _: A & IsSomeSameTypeContainerArray<A>
) {}


test([
  {
    val1: "string",
    val2: "also string"
  },
  {
    val1: 5,
    val2: 3
  },
  {
    val1: 3,  // error... not number & string!!
    val2: "4" // error... not string & number!!
  }
]);

Ссылка на игровую площадку

Я думаю, это работает так, как вы хотите.Это немного сложно, но я в основном объяснил это в строке.IsSomeSameTypeContainerArray<A> - это сопоставленный массив , который использует условный вывод типа для каждого элемента для преобразования {val1: T1, val2: T2} в {val1: T2, val2: T1}.Если это преобразование не меняет тип A, тогда все хорошо.В противном случае будет хотя бы один элемент, который не соответствует элементу замененных типов, и возникнет ошибка.

В любом случае, надеюсь, это поможет;удачи!

...