【问题标题】:Use nested generic interface to allow only certain types in an array使用嵌套泛型接口仅允许数组中的某些类型
【发布时间】:2019-09-24 06:17:51
【问题描述】:

要求:对于传递给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 Playground @jcalz provided for the nested array use case

Attempt Playground


澄清:以上是我尝试对数组的第二个元素执行错误。但是,它确实出错,但原因是错误的。这是我最初的尝试,根本没有给出错误:

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

【问题讨论】:

  • 在“// should error since isn't a number”中,它确实错误。你能做一些不会出错但应该出错的东西吗?否则我很难跟上。
  • 感谢您的浏览!我在上面添加了说明。

标签: typescript typescript-generics


【解决方案1】:

呃,这太累了。我不知道做这种事情是否值得。现有类型是对此的“正确”解决方案,但它们在 TS 中的编码方式需要您将类型更改为更像 Promise 的模型(其中不是 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!
  ],
});

Playground link

是吗?它太复杂了,我可能会认为你要么想把它塞进图书馆,让凡人不需要看它......


...或者您可能想重构为其他内容,说出将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。请注意我必须如何在createStore() 上指定&lt;any, any&gt;...这是因为它期望S 是像stringboolean 这样的单一事物,而不是string | boolean,这是它需要的.因此,您可能需要在那里进行一些重构,以准确指定您要限制 S 的内容。

希望有所帮助;再次祝你好运。

【讨论】:

  • 抱歉让你筋疲力尽@jcalz!但我真的很感谢你在这里的知识。我很乐意给你买杯咖啡 :) 我开始理解更多使用 extendsinfer 来缩小可接受的类型。我尝试将第一段代码添加到新的test.ts 文件中,然后使用tsc test.ts 进行编译,但出现错误。我觉得有趣的是,预编译器中发现了错误。
  • 错误:Type '({ selector: Selector&lt;string, boolean&gt;; value: boolean; } | { selector: Selector&lt;boolean, number&gt;; value: number; })[]' is missing the following properties from type '[{ selector: Selector&lt;string, boolean&gt;; value: boolean; }, { selector: Selector&lt;boolean, number&gt;; value: number; }]': 0, 1
  • 我在上面的代码中添加了游乐场链接以显示它的工作原理。它可能只在 TS3.4+ 上按预期工作,因为这就是我正在使用的。可能需要对其进行修改才能在早期版本的 TS 中工作,但我目前不能花太多时间在它上面。
猜你喜欢
  • 1970-01-01
  • 2020-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-09
  • 2013-04-27
  • 1970-01-01
  • 1970-01-01
  • 2015-04-28
相关资源
最近更新 更多