英文:
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
可以明确地编写为从TagInputMap
到TagOutputMap
的映射类型:
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")
的联合类型并失败)。将类型为K
的tag
分配给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["_tag"]]: T }
From this we can then start describing what we're doing. Let's define TagInput<K>
as the intended input to createValueOfObjKey
as follows:
type TagInput<K extends keyof TagInputMap> = { _tag: K } & 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]) => TagOutputMap[P]
} = {
_list: ListOutput,
_number: NumberOutput,
_string: StringOutput
}
and finally the function implementation:
function createValueOfObjKey<K extends keyof TagInputMap>(i: TagInput<K>) {
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 & InputMap[K]["_tag"]
which causes things to break (it will fall back to a union like K & ("_string" | "_number" | "_list")
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: "_list" })
// const ret: ListOutput
Looks good.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论