如何在TypeScript中使用直接键访问和通用接口正确推断键的类型?

huangapple go评论65阅读模式
英文:

How to correctly infer the type of a key with direct key access and generic interface in TypeScript?

问题

我明白你在尝试解决的问题。在你提供的代码中,当尝试调用selected.callback(selected.firstMapper())时出现了一个类型错误。你认为 TypeScript 在处理 configKey"b" 的情况时,无法正确地推断类型。

你尝试了一些方法,包括创建一个更通用的接口,但并未取得成功。

你在这个小型 Playground中复现了这个问题。

请问你希望在这方面获得什么样的帮助?

英文:

I am trying to get TypeScript to correctly infer the types that are valid in the following context:

I have 2 interfaces

interface A {
  first: string;
}

type B = {
  second: string;
}

as well as a generic interface that has 2 mappers where the types depend on each other, something like

interface Gen<T extends string> {
  firstMapper: () => T;
  callback(arg: T): string;
}

I want also to have an interface that records multiple Gen interfaces

interface Aobject {
  "a": Gen<"a">,
  "b": Gen<"b">,
}

Finally, I want to create function that takes a key of AObject, retrieves the configuration from the AObject interface calls the firstMapper and then the callback such as:

const buildMapping = (configKey: keyof Aobject, arg: Aobject) => {
  const myA: Aobject = {
    "a": {
      firstMapper: () => "a",
      callback: (input) => ("result" + input)
    },
    "b": {
      firstMapper: () => "b",
      callback: (input) => ("result" + input)
    }
  }

  const selected = myA[configKey];
  selected.callback(selected.firstMapper());
}

However, when I try to do this, I have an error

Argument of type 'string' is not assignable to parameter of type 'never'.
  Type 'string' is not assignable to type 'never'.

However, if I change to this

  if (configKey === "a") {
    const selected = myA[configKey]
  selected.callback(selected.firstMapper())
  }
  if (configKey === "b") {
    const selected = myA[configKey]
    selected.callback(selected.firstMapper())
  }

there is no error.

My understanding is that Typescript does not understand that if the configKey is of type "b", then selected can only have the type


b {
   firstMapper: () => "b",
   callback: (input) => ("result" + input)
}

instead TS "thinks" that the callback can be called with both and tries to create the intersection of type "a" and type "b" which results in never.

I am wondering if there is a better way to handle this use case, because I find it weird having to write an if case executing the same piece of code.

I tried to create a more generic interface because I thought the problem might come from the AObject definition but no success.

I created a small playground to reproduce the issue.

答案1

得分: 0

以下是翻译好的内容:

  1. 满足编译器的两种方法:

    1. 您可以使用 类型断言

      TS Playground

      type Gen<T extends string> = {
        firstMapper: () => T;
        callback(arg: T): string;
      };
      
      type Aobject = {
        a: Gen<"a">;
        b: Gen<"b">;
      };
      
      function buildMapping<K extends keyof Aobject>(
        configKey: K,
        arg: Aobject,
      ) {
        const selected = arg[configKey] as Gen<K>;
        //                              ^^^^^^^^^
        selected.callback(selected.firstMapper());
      }
      
  2. 您可以将类型 Aobject 定义为映射类型,编译器将正确推断,无需断言。生成的类型相同:

    TS Playground

    type Gen<T extends string> = {
      firstMapper: () => T;
      callback(arg: T): string;
    };
    
    type Aobject = { [Str in "a" | "b"]: Gen<Str> };
      //^? type Aobject = { a: Gen<"a">; b: Gen<"b">; }
    
    function buildMapping<K extends keyof Aobject>(
      configKey: K,
      arg: Aobject,
    ) {
      const selected = arg[configKey];
      selected.callback(selected.firstMapper());
    }
    
英文:

Here are two ways to satisfy the compiler:

  1. You can use a type assertion:

    TS Playground

    type Gen&lt;T extends string&gt; = {
      firstMapper: () =&gt; T;
      callback(arg: T): string;
    };
    
    type Aobject = {
      a: Gen&lt;&quot;a&quot;&gt;;
      b: Gen&lt;&quot;b&quot;&gt;;
    };
    
    function buildMapping&lt;K extends keyof Aobject&gt;(
      configKey: K,
      arg: Aobject,
    ) {
      const selected = arg[configKey] as Gen&lt;K&gt;;
      //                              ^^^^^^^^^
      selected.callback(selected.firstMapper());
    }
    
    
  2. You can define the type Aobject as a mapped type and the compiler will infer correctly without the assertion. The resulting type is the same:

    TS Playground

    type Gen&lt;T extends string&gt; = {
      firstMapper: () =&gt; T;
      callback(arg: T): string;
    };
    
    type Aobject = { [Str in &quot;a&quot; | &quot;b&quot;]: Gen&lt;Str&gt; };
      //^? type Aobject = { a: Gen&lt;&quot;a&quot;&gt;; b: Gen&lt;&quot;b&quot;&gt;; }
    
    function buildMapping&lt;K extends keyof Aobject&gt;(
      configKey: K,
      arg: Aobject,
    ) {
      const selected = arg[configKey];
      selected.callback(selected.firstMapper());
    }
    
    
    

huangapple
  • 本文由 发表于 2023年5月10日 16:23:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76216335.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定