【问题标题】: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

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案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:

    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"
    

    【讨论】:

      【解决方案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 的类型,那么一切都很好。否则至少会有一个元素与交换类型的元素不匹配,就会出现错误。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-10-27
        • 1970-01-01
        • 2017-11-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-13
        相关资源
        最近更新 更多