Typed function to create output based on input with refinement and controlled return types

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

Typed function to create output based on input with refinement and controlled return types

问题

以下是您要翻译的代码部分:

interface StringInput {
  _tag: "string";
}

interface NumberInput {
  _tag: "number";
}

interface ListInput {
  _tag: "list";
}

type Input = StringInput | NumberInput | ListInput;

class StringOutput {
  _tag = "StringOutput" as const;
  constructor(_: StringInput) {}
}

class NumberOutput {
  _tag = "NumberOutput" as const;
  constructor(_: NumberInput) {}
}

class ListOutput {
  _tag = "ListOutput" as const;
  constructor(_: ListInput) {}
}

type Output = StringOutput | NumberOutput | ListOutput;

希望这对您有所帮助。如果您有其他问题或需要进一步的帮助,请随时提问。

英文:

I have a bunch of different inputs:

interface StringInput {
  _tag: "_string";
}

interface NumberInput {
  _tag: "_number";
}

interface ListInput {
  _tag: "_list";
}

type Input = StringInput | NumberInput | ListInput;

Along with the corresponding outputs:

class StringOutput {
  _tag = "StringOutput" as const;
  constructor(_: StringInput) {}
}

class NumberOutput {
  _tag = "NumberOutput" as const;
  constructor(_: NumberInput) {}
}

class ListOutput {
  _tag = "ListOutput" as const;
  constructor(_: ListInput) {}
}

type Output = StringOutput | NumberOutput | ListOutput;

I'd like to have a function that creates the right output given an input. The function should be well-typed especially in its implementation.

 

I'm stuck with two attemps, each of which solves half of the problem.

Attempt 1: switch. It is a well-known improvement possibility for TypeScript. The input parameter i get refined, which is something I need in order to call constructor of the corresponding output object. On the other hand the generic I does not get refined, causing the problem with the return type.

namespace Attempt1 {

  type RemoveUn<S> = S extends `_${infer R}` ? R : S

  type InputOutputMap = {
    [K in Input["_tag"]]: Extract<Output,  { _tag: `${Capitalize<RemoveUn<K>>}Output` }>
  }

  function createValueOfObjKey<
    const I extends Input
  >(i: I): InputOutputMap[I["_tag"]] {
    switch(i._tag) {
      case "_string": {
        return new StringOutput(i); // error
      }
      case "_number": {
        return new NumberOutput(i); // error
      }
      case "_list": {
        return new ListOutput(i); // error
      }
    }
  }

}

Attempt 2: indexed access types. The compiler does quite well controlling them, so what I return must be in line with what I have stated in the signature. But I lost the refinement on i and I'm not able to restore it.

namespace Attempt2 {

  type RemoveUn<S> = S extends `_${infer R}` ? R : S

  type InputOutputMap = {
    [K in Input["_tag"]]: Extract<Output,  { _tag: `${Capitalize<RemoveUn<K>>}Output` }>
  }

  function createValueOfObjKey<
    const I extends Extract<Input, { _tag: K }>,
    const K extends Input["_tag"] = I["_tag"]
  >(i: I): InputOutputMap[K] {
    return {
      get _string() {
        return new StringOutput(i as StringInput);
      },
      get _number() {
        return new NumberOutput(i as NumberInput);
      },
      get _list() {
        return new ListOutput(i as ListInput);
      }
    }[i._tag];
  }

}

 

Any idea? TypeScript playground

答案1

得分: 1

i._tag以及输入和输出类型之间的关系在TypeScript中不太容易支持。我认为这与我所谓的“相关联联合类型”密切相关,如microsoft/TypeScript#30581中所述。从概念上讲,您可以将这三个构造函数放在名为ctors的对象中,然后让您的函数返回new ctors[i._tag](i),它应该能够进行类型检查。在实际操作中,编译器并不真正直接理解它。

您可以重构以使用泛型和索引访问类型,如microsoft/TypeScript#47109中所述,但这并不是非常直接。思路是创建一个“基本”映射类型,类似于您的InputOutputMap,但用于输入。我将其称为TagInputMap(并将您的重命名为TagOutputMap,因为它是类似的):

type TagInputMap = { [T in Input["_tag"]]: T }

从这里,我们可以开始描述我们正在做的事情。让我们将TagInput<K>定义为createValueOfObjKey的预期输入,如下所示:

type TagInput<K extends keyof TagInputMap> = { _tag: K } & TagInputMap[K]

这几乎可以直接是TagInputMap[K],但是当您调用createValueOfObjKey(i)时,编译器将无法推断出K(请参阅microsoft/TypeScript#51612)。因此,“多余”的交叉类型就有了存在的必要性。

现在,ctors可以明确地编写为从TagInputMapTagOutputMap的映射类型:

const ctors: {
  [P in keyof TagInputMap]:
  new (_: TagInputMap[P]) => TagOutputMap[P]
} = {
  _list: ListOutput,
  _number: NumberOutput,
  _string: StringOutput
}

最后是函数实现:

function createValueOfObjKey<K extends keyof TagInputMap>(i: TagInput<K>) {
  const tag: K = i._tag
  return new ctors[tag](i)
}

再次强调,这几乎可以是new ctors[i._tag](i),但是交叉类型会让编译器感到困惑。我们希望在这里仅将i._tag看作K,但编译器会将其视为K & InputMap[K]["_tag"],这会导致问题(它将回退到一个类似于K & ("_string" | "_number" | "_list")的联合类型并失败)。将类型为Ktag分配给tag是最迅速且仍然类型安全的修复方式。

现在让我们从调用者的角度尝试一下:

const ret = createValueOfObjKey({ _tag: "_list" })
// const ret: ListOutput

看起来不错。

英文:

The sort of relationship between i._tag and the input and output types isn't very easily supported by TypeScript. I consider this strongly related to what I call "correlated union types" as described in microsoft/TypeScript#30581. Conceptually you could just put the three constructors in an object called ctors and then have your function return new ctors[i._tag](i) and it should type check. In practice the compiler doesn't really understand it directly.

You can refactor to use generics and indexed access types as described in microsoft/TypeScript#47109, but it's not incredibly straightforward. The idea is to come up with a "basic" mapping type , like your InputOutputMap but for the input. I'll call it TagInputMap (and rename yours to TagOutputMap, since it's analogous):

type TagInputMap = { [T in Input as T[&quot;_tag&quot;]]: T }

From this we can then start describing what we're doing. Let's define TagInput&lt;K&gt; as the intended input to createValueOfObjKey as follows:

type TagInput&lt;K extends keyof TagInputMap&gt; = { _tag: K } &amp; TagInputMap[K]

This could almost just be TagInputMap[K], but then when you call createValueOfObjKey(i), the compiler would fail to infer K (see microsoft/TypeScript#51612). Hence the "redundant" intersection.

Now ctors can be explicitly written as a mapped type from TagInputMap to TagOutputMap:

const ctors: {
  [P in keyof TagInputMap]:
  new (_: TagInputMap[P]) =&gt; TagOutputMap[P]
} = {
  _list: ListOutput,
  _number: NumberOutput,
  _string: StringOutput
}

and finally the function implementation:

function createValueOfObjKey&lt;K extends keyof TagInputMap&gt;(i: TagInput&lt;K&gt;) {
  const tag: K = i._tag
  return new ctors[tag](i)
}

Again, this could almost be new ctors[i._tag](i), but the intersection confuses the compiler. We want i._tag to be seen only as K here, but the compiler sees it as K &amp; InputMap[K][&quot;_tag&quot;] which causes things to break (it will fall back to a union like K &amp; (&quot;_string&quot; | &quot;_number&quot; | &quot;_list&quot;) and fail). The assignment to tag of type K is the most expedient and still type-safe way to fix it.


And let's try it out from the caller's side:

const ret = createValueOfObjKey({ _tag: &quot;_list&quot; })
// const ret: ListOutput

Looks good.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月29日 00:40:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76575171.html
匿名

发表评论

匿名网友

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

确定