使用泛型在接口数组中强制实施相同类型的属性

时间:2019-05-03 00:35:10

标签: typescript typescript-generics

我正在寻求使用泛型来强制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,因为这是数组中第一个元素的定义方式。

我希望评估数组中的每个元素是否独立满足给定的泛型。

任何帮助将不胜感激!


更新:

感谢您的帮助!我很感激!我开始理解使用扩展,但是无法将其扩展到我的实际用例中:

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的类型匹配。

Ex:如果selector属性的类型为Selector<boolean, number>,则value属性的类型应该为number,而与数组其他元素的类型无关。

Typescript Playground

这是我第一次尝试为上述嵌套用例提供的Typescript游乐场@jcalz:

Attempt Playground

3 个答案:

答案 0 :(得分:0)

Array<SameTypeContainer<any>>不起作用的原因是,因为实际上任何值都可以分配给any,所以无论{{1}是什么,{val1: x, val2: y}的类型都是SameTypeContainer<any> }}和x是。


您要查找的类型是一个数组,其中每个元素都是 some y类型,但没有任何 specific SameTypeContainer<T>类型。最好用T之类的existential type来表示,目前TypeScript(其他大多数带有泛型的语言)都不支持本机。 TypeScript(以及大多数其他具有泛型的语言)仅具有 universal 类型:想要一个Array<SameTypeContainer<exists T>>类型的值的人可以为他们想要的X<T>指定任何类型,并且提供程序价值必须能够遵守。存在类型则相反:有人想要提供诸如T之类的类型的值,可以为他们想要的X<exists T>选择任何特定类型,而该值的接收者只需遵守即可。但是,TypeScript没有存在类型,因此我们还必须做其他事情。

(嗯,它没有 native 存在类型。您可以使用泛型函数并通过回调反转控制来emulate them,但使用起来比我的解决方案还要复杂接下来会提出建议。如果您仍然对存在性感兴趣,可以阅读有关它的链接文章。


我们要做的第二件事是使用泛型类型推断,方法是让T是一个接受泛型类型test()并扩展了 {{ 1}},然后验证A是否与所需约束匹配。这是我们可以做到的一种方法:

Array<SameContainer<any>>

Playground link

我认为这可以按照您想要的方式工作。这有点复杂,但是我主要是在线解释的。 A是一个mapped array,它在每个元素上使用conditional type inferenceinterface 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}的类型,那么一切都很好。否则,将至少有一个与交换类型的元素不匹配的元素,并且会出现错误。

无论如何,希望能有所帮助;祝好运!

答案 1 :(得分:0)

发生了什么事,打字稿正在尝试最好为您推断类型,因此,它只是将泛型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)[]) => {

}

答案 2 :(得分:0)

自@jcalz提出以来,请进行一些存在性输入! I've already posted this answer,所以我将担任这一CW。其他答案可能更惯用(因此更好)。但从理论上讲它是合理的,因此应该是正确的

您已经有了参数类型:

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

存在“通用SameTypeContainer个消费者”,它们具有以下通用量化类型(由其返回类型进行参数化设置)

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

如果您有SameTypeContainer<T>,但不知道T是什么,那么唯一可以做的就是将其传递到SameTypeConsumer<R>中,而不会{ em> care T是什么,然后取回R(不依赖于T)。因此,SameTypeContainer<T>-未知-T等效于一个函数,该函数接受不需要关心的任何T使用者,并自行运行:< / p>

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)

This should output

  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"