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

Posted

技术标签:

【中文标题】使用泛型在接口数组中强制执行相同类型的属性【英文标题】:Use generics to enforce same type properties in an array of an interface 【发布时间】:2019-09-21 12:46:10 【问题描述】:

我希望使用泛型来强制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 为字符串类型,val2 为字符串类型,因为这是数组中第一个元素的定义方式。

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

任何帮助将不胜感激!


更新:

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

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&lt;boolean, number&gt; 类型,则value 属性应该是number 类型,与数组类型的其他元素无关。

Typescript Playground

这是我第一次尝试修改为上述嵌套用例提供的 Typescript Playground @jcalz:

Attempt Playground

【问题讨论】:

【参考方案1】:

自从@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&lt;T&gt;,但您不知道T 是什么,那么您唯一能做的就是将它传递给SameTypeConsumer&lt;R&gt;,这并不关心 T 是什么,并得到一个 R(不依赖于 T)。所以,SameTypeContainer&lt;T&gt;-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)

“埋葬”这样的类型允许您将所有不同类型的 SameTypeContainers 注入到一个大联合类型 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:

    不匹配 匹配项 不匹配

您可能会发现进一步定义很有用

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"

【讨论】:

【参考方案2】:

正在发生的事情是 typescript 正在尽力为您推断类型,因此它只是将通用 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)[]) => 


【讨论】:

如果第二个元素的 val1 和 val2 不是字符串,这不起作用【参考方案3】:

Array&lt;SameTypeContainer&lt;any&gt;&gt; 不起作用的原因是因为实际上任何值都可以分配给any,所以无论xy 是什么类型,val1: x, val2: y 的类型都是SameTypeContainer&lt;any&gt;


您要查找的类型是一个数组,其中每个元素都是一些 SameTypeContainer&lt;T&gt; 类型,但不是任何特定 T。这可能最好表示为existential type,例如(可能)Array&lt;SameTypeContainer&lt;exists T&gt;&gt;,目前 TypeScript(也包括大多数其他具有泛型的语言)本身不支持它。 TypeScript(以及大多数其他具有泛型的语言)只有 universal 类型:想要X&lt;T&gt; 类型值的人可以为T 指定他们想要的任何类型,并且值的提供者必须能够遵守。存在类型则相反:想要提供X&lt;exists T&gt; 这样类型的值的人可以为T 选择他们想要的任何特定类型,并且接收者 该值必须遵守。但是,TypeScript 没有存在类型,所以我们必须做其他事情。

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


我们可以做的下一个最好的事情是使用泛型类型推断,让test() 成为一个泛型函数,接受泛型类型A 的参数,扩展 Array&lt;SameContainer&lt;any&gt;&gt;,然后验证 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!!
  
]);

Playground link

我想,这就是你想要的方式。这有点复杂,但我主要是内联解释它。 IsSomeSameTypeContainerArray&lt;A&gt; 是一个mapped array,它在每个元素上使用conditional type inference 将val1: T1, val2: T2 转换为val1: T2, val2: T1。如果该转换没有改变A 的类型,那么一切都很好。否则至少会有一个元素与交换类型的元素不匹配,就会出现错误。

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

【讨论】:

以上是关于使用泛型在接口数组中强制执行相同类型的属性的主要内容,如果未能解决你的问题,请参考以下文章

java学习第16天(泛型 增强for)

Java泛型

Java泛型

JavaSE基础八----<集合>泛型集合体系Collection接口中的方法

打字稿:允许泛型类型仅是具有“字符串”属性的对象

Typescript接口,强制执行额外属性的类型[重复]