【问题标题】:Typescript Syntax for Type Inference based on kind基于种类的类型推断的打字稿语法
【发布时间】:2021-11-25 14:48:47
【问题描述】:

我在编写正确的 typescript 语法以严格推断 where 时遇到问题:

  1. 编译器正确报告缺少的 switch/case 选项
  2. 返回值按类型匹配输入类型
type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;

function create<T extends AllKinds>(kind:AllKinds['kind']):T {
  switch(kind) {
    case "a": return {kind:'a'};
    case "b": return {kind:'b'};
    case "c": return {kind:'c'};
  }
}

create("a");

Playground

我想知道最新的 Typescript 是否可以做到这一点。

使用我的其他方法(即case "a": return {kind:'b'} as T;),返回的值未根据我的需要进行类型检查。

【问题讨论】:

    标签: typescript type-inference union-types


    【解决方案1】:

    在您的情况下返回T 是不安全的。

    看看为什么:

    type KindA = { kind: 'a' };
    type KindB = { kind: 'b' };
    type KindC = { kind: 'c' };
    type AllKinds = KindA | KindB | KindC;
    
    function create<T extends AllKinds>(kind: AllKinds['kind']): T {
      switch (kind) {
        case "a": return { kind: 'a' };
        case "b": return { kind: 'b' };
        case "c": return { kind: 'c' };
      }
    }
    
    const result = create<{ kind: 'a', WAAAT: () => {} }>("a")
    result.WAAAT() // compiler but causes an error in runtime
    

    通用参数在 90% 的情况下应取决于输入值。

    看这个例子:

    type KindA = { kind: 'a' };
    type KindB = { kind: 'b' };
    type KindC = { kind: 'c' };
    type AllKinds = KindA | KindB | KindC;
    
    
    const builder = <Kind extends AllKinds['kind']>(kind: Kind) => ({ kind })
    
    const result = builder("a"); // {kind: 'a' }
    
    

    Playground

    请参阅this 答案、this 答案和我的article 了解更多上下文

    我说得对吗,女巫当前的 Typescript,不能两者兼得?

    问题不在于switch 语句,而在于显式返回类型。返回类型不能依赖于未绑定函数参数的泛型值。

    其实你想要的都是可以实现的:

    type KindA = { kind: 'a' };
    type KindB = { kind: 'b' };
    type KindC = { kind: 'c' };
    type AllKinds = KindA | KindB | KindC;
    
    function create<Kind extends AllKinds['kind']>(kind: Kind): Extract<AllKinds, { kind: Kind }>
    function create(kind: AllKinds['kind']) {
        switch (kind) {
            case "a": return { kind: 'a' };
            case "b": return { kind: 'b' };
            case "c": return { kind: 'c' };
        }
    }
    
    const result1 = create("a") // KindA
    const result2 = create("b") // KindB
    const result3 = create("c") // KindC
    
    

    Playground

    您可能已经注意到,我使用了function overloading。它使 TS 编译器不那么严格。换句话说,提供了一些不安全性,但同时使其更具可读性并推断返回类型。

    AFAIK,函数重载行为bivariantly。因此,由您决定哪个选项更好

    【讨论】:

    • 感谢您的快速回答。我是否正确地理解了当前的 Typescript,不能同时拥有两者:1. switch/case 同时用于 kind 2. 返回类型检查?或者我的原始问题中的两点可以以某种方式匹配吗?
    • @Yoz 更新了
    • 这个版本可以让我做case "a": return { kind: 'b' };
    • 这就是为什么它是不安全的方法。就像我说的
    • 谢谢,知道了,它...我会保持开放,看看是否有人能想出一个同时匹配 1. 和 2. 的想法。
    【解决方案2】:

    错误描述性很强。

    Type '{ kind: "a"; }' is not assignable to type 'T'.
      '{ kind: "a"; }' is assignable to the constraint of type 'T',
      but 'T' could be instantiated with a different subtype of constraint 'AllKinds'.
    

    即使使用更简单的类型约束也会出现此问题。

    function foo<T extends string>(): T {
      return 'foo';
    }
    

    这里我们会得到以下错误。

    Type 'string' is not assignable to type 'T'.
      'string' is assignable to the constraint of type 'T',
      but 'T' could be instantiated with a different subtype of constraint 'string'.
    

    问题是我们说我们会返回T 类型的东西,但Tstring 的类型不同。是的,T 类型扩展了string,但这意味着Tstring 的子类型。例如,'bar' 类型是string 的子类型。因此,我们可以用'bar' 实例化T。因此,我们希望返回值为'bar',但返回值为'foo'

    解决方案就是不使用泛型。如果您想返回string,那么只需说您要返回string。不要说您正在返回某个子类型 Tstring 的值。

    function foo(): string {
      return 'foo';
    }
    

    同样,如果您想返回一个AllKinds 类型的值,那么只需说您要返回一个AllKinds 类型的值。不要说您正在返回某个子类型 TAllKinds 的值。

    type KindA = {kind:'a'};
    type KindB = {kind:'b'};
    type KindC = {kind:'c'};
    type AllKinds = KindA | KindB | KindC;
    
    function create(kind:AllKinds['kind']): AllKinds {
      switch(kind) {
        case "a": return {kind:'a'};
        case "b": return {kind:'b'};
        case "c": return {kind:'c'};
      }
    }
    
    create("a");
    

    编辑:你需要依赖类型来做你想做的事。 TypeScript 没有依赖类型。但是,您可以创建一个自定义折叠函数来提供额外的类型安全性。

    type Kind = 'a' | 'b' | 'c';
    
    type KindA = { kind: 'a' };
    type KindB = { kind: 'b' };
    type KindC = { kind: 'c' };
    
    type AllKinds = KindA | KindB | KindC;
    
    function foldKind<A, B, C>(a: A, b: B, c: C): (kind: Kind) => A | B | C {
      return function (kind: Kind): A | B | C {
        switch (kind) {
          case 'a': return a;
          case 'b': return b;
          case 'c': return c;
        }
      }
    }
    
    const create: (kind: Kind) => AllKinds = foldKind<KindA, KindB, KindC>(
      { kind: 'a' },
      { kind: 'b' },
      { kind: 'c' }
    );
    

    现在,您只能为'a' 提供KindA 的值,为'b' 提供KindB 的值,为'c' 提供KindC 的值。亲自查看demo

    【讨论】:

    • 感谢您的快速回答。我是否正确地理解了当前的 Typescript,不能同时拥有两者:1. switch/case 同时用于 kind 2. 返回类型检查?或者我的原始问题中的两点可以以某种方式匹配吗?
    • 你可以同时拥有,如我上面的回答所示。我不知道您为什么要尝试使用泛型类型。
    • 使用建议的语法,可以做到case "a": return {kind:'b'}; - 意味着编译器没有按照我的意愿进行保护。 (见我的观点 2)
    • 你需要依赖类型来做你想做的事。 TypeScript 没有依赖类型。但是,您可以创建一个自定义折叠函数来提供额外的类型安全性。详情请参阅我的回答。
    • 感谢您调查 Aadit。似乎仍然不够通用(想象一下我有 10+、100+ 种),而且 create("a") 返回 AllKind 而不是 KindA。我知道这是语言的当前限制,所以我创建了一个功能请求github.com/microsoft/TypeScript/issues/46236
    猜你喜欢
    • 2018-08-12
    • 2021-03-10
    • 1970-01-01
    • 2018-06-09
    • 1970-01-01
    • 2020-04-02
    • 2020-12-20
    • 2016-08-03
    • 2019-04-10
    相关资源
    最近更新 更多