Используйте вложенный универсальный интерфейс, чтобы разрешить только определенные типы в массиве - PullRequest
0 голосов
/ 06 мая 2019

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

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

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
  ],
});

Typescript Playground

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

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


Уточнение. Выше приведена моя попытка вызвать ошибку во втором элементе массива.Тем не менее, это ошибка, но по неправильной причине.Вот моя первоначальная попытка, где вообще не сообщается об ошибке:

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

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

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

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

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

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 unless value is a number.
    //the passed `selector` property is type Selector<boolean, number>, therefor, the `value` should be a number
    //the second type of the selector property should match the type of value
  ],
});

Вот Typescript Playground .

1 Ответ

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

Тьфу, это утомительно.Я не знаю, стоит ли делать такие вещи.Экзистенциальные типы - это «правильное» решение этой проблемы, но способ, которым они кодируются в TS, потребует, чтобы вы изменили свои типы на более обещающую модель (где вместо значения типа T у вас есть функция, которая принимает обратный вызовкоторый действует на T).

В любом случае, эта часть не изменилась:

// unchanged:

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

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

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

И эта часть определенно изменилась:

// changed:

export interface Config<T, S, Result> {
  initialState?: T;
  // changed below, prefer inferring tuples over just arrays
  selectorsWithValue?: SelectorWithValue<S, Result>[] | [SelectorWithValue<S, Result>];
}

// drill down into a Config, and make sure value R is assignable to the R in Selector<S, R>
type ConfigOkay<C extends Config<any, any, any>> =
  C extends { selectorsWithValue?: infer A } ?
  A extends SelectorWithValue<any, any>[] ?
  {
    selectorsWithValue?: { [I in keyof A]: A[I] extends {
      selector: Selector<infer S, infer R1>, value: infer R2
    } ? { selector: Selector<S, R1>, value: R1 } : never }
  } : never : never;


export function createStore<C extends Config<T, S, any>, T = any, S = any>(
  config: C & ConfigOkay<C> = {} as any // assertion:
  // default value {} is not seen as C & ConfigOkay<C> I guess
): Store<T, S, any> {
  return new Store(config.initialState, config.selectorsWithValue);
}

Ивот оно в действии.

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: 1 } // okay
  ],
});

createStore({
  selectorsWithValue: [
    { selector: selectBooleanFromString, value: false },
    { selector: selectNumberFromBoolean, value: "string" } // error!
  ],
});

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

Уууу?Это настолько сложно, что я, возможно, подумаю, что вы либо захотите засунуть это в библиотеку, где смертным не нужно смотреть на это ...


... или вы захотите рефакторинг на что-то другое,Скажите что-нибудь, что пометит SelectorWithValue как «хороший», а затем примет только «хорошие»:

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

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

export interface GoodSelectorWithValue<S> {
  knownGood: true
  selector: Selector<S, any>;
  value: any
}

function vetSV<S, R>(x: SelectorWithValue<S, R>): GoodSelectorWithValue<S> {
  return Object.assign({ knownGood: true as true }, x);
}

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

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

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

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

createStore<any, any>({
  selectorsWithValue: [
    vetSV({ selector: selectBooleanFromString, value: false }),
    vetSV({ selector: selectNumberFromBoolean, value: 1 })
  ]
}); // okay

createStore<any, any>({
  selectorsWithValue: [
    vetSV({ selector: selectBooleanFromString, value: false }),
    vetSV({ selector: selectNumberFromBoolean, value: "string" }) // error!
  ]
}); 

Playground link

Это может быть лучше, где вам нужно, чтобы люди проделали большую работу для создания SelectorWithValue.Обратите внимание, как я должен был указать <any, any> для createStore() ... это потому, что он ожидает, что S будет единственной вещью, такой как string или boolean, а не string | boolean, как это и должно быть,Поэтому вам может понадобиться рефакторинг, чтобы точно указать, что вы пытаетесь ограничить S.

Надеюсь, это поможет;снова удачи.

...