英文:
Type inference on function parameters with nested generics?
问题
我面临了与TypeScript泛型相关的问题。
我有一个必须根据类型和一些依赖于此类型的属性来调用的函数。
当在条件语句中对类型进行保护以匹配 TYPES.ME
时,TypeScript 无法推断属性的类型:
enum TYPES {
ME = 'me',
YOU = 'you'
}
type Attributes<T extends TYPES> = T extends TYPES.ME ? {keys: true} : {hat: true}
const func = <T extends TYPES>(type: T, attr: Attributes<T>) => {
if(type === TYPES.ME) {
attr.keys // 错误:属性 'keys' 不存在于类型 '{ keys: true; } | { hat: true; }'
}
}
我想听听您对这个问题的意见,看看是否有一个好的解决方法。
祝好!
英文:
I am facing an issue with Typescript generics.
I have a function which must be called with a type and some attributes depending on this type.
Typescript does not manage to infer the type of attributes when inside an if guarding the type to TYPES.ME
:
enum TYPES {
ME = 'me',
YOU = 'you'
}
type Attributes<T extends TYPES> = T extends TYPES.ME ? {keys: true} : {hat: true}
const func = <T extends TYPES>(type: T, attr: Attributes<T>) => {
if(type === TYPES.ME) {
attr.keys // error Property 'keys' does not exist on type '{ keys: true; } | { hat: true; }'
}
}
I would like to have your opinions on this matter and see if there is a nice workaround.
Cheers!
答案1
得分: 1
目前,TypeScript 无法根据控制流分析重新约束 泛型 类型参数。在 func(type, attr)
的函数体内,您检查 type === Types.ME
。这可以将 type
的类型从 T
缩小到类似于 T & Types.ME
。**但它无法改变 T
本身。**类型参数 T
仍然保持不变;它未被约束为 Types.ME
。因此编译器无法得出 attr
的类型是 Attributes<Types.ME>
。
从技术上讲,编译器拒绝更改 T
是正确的。这是因为,虽然像 type
这样的个别值一次只能是一种类型,但像 T
这样的类型参数可以是 联合类型。事实上,您可以使用等于完整 Types.ME | Types.YOU
联合的 T
调用 func()
,就像这样:
func(
Math.random() < 0.999 ? Types.ME : Types.YOU,
{ hat: true }
) // 没有错误编译
如果您检查这个调用,您将看到 T
被推断为 Types
(Types.ME | Types.YOU
的完整联合),因此即使在 type
是 Types.ME
的情况下,也允许 attr
是 {hat: true}
,即使这种情况非常可能。
有一个长期存在的功能请求在 microsoft/TypeScript#27808 中提到,请求一种方式来表示 "T
将确切地是 Types.ME
或 Types.YOU
之一;它不能是一个联合"。然后,也许在函数体内,检查 type === Types.ME
将允许将 T
本身约束为 Types.ME
,并且一切都会按预期工作。预计使用 Math.random() < 0.999
的调用将被拒绝。
但目前这不是该语言的一部分。
您可以考虑采用一种不同的方法,而不是使 func
成为泛型,可以将其类似于 重载函数 的函数,其中每个 Types
成员都有一个调用签名。您可以编写一个带有 剩余参数 类型为 辨识联合 的 元组类型 的函数,并且编译器将在函数体内将其视为这样处理。
也许像这样:
type FuncArg =
[type: Types.ME, attr: { keys: true }] |
[type: Types.YOU, attr: { hat: true }];
const func: (...args: FuncArg) => void = (type, attr) => {
if (type === Types.ME) {
console.log(attr.keys) // 这可以工作
}
}
现在,您无法进行无效调用:
func(
Math.random() < 0.999 ? Types.ME : Types.YOU,
{ hat: true }
) // 错误,类型 'Types.ME' 无法分配给类型 'Types.YOU'
一切都正常工作,因为现在 type
和 attr
被绑定在 FuncArg
中,就像想象 T
被约束为 Types
中的一个成员一样,并且可以遍历可能性。注意,如果需要,FuncArg
可以从问题中给出的 Attributes
类型或其他映射接口计算,但这超出了问题的范围。
英文:
Currently, TypeScript is unable to re-constrain generic type parameters as a result of control flow analysis. Inside the body of func(type, attr)
, you check that type === Types.ME
. This can narrow the type of type
from T
to something like T & Types.ME
. But it cannot do anything to T
itself. The type parameter T
stubbornly stays the same; it is not constrained to Types.ME
. And thus the compiler cannot conclude that attr
is of type Attributes<Types.ME>
.
And it is technically correct for the compiler to refuse to change T
. That's because, while individual values like type
can only be one thing at a time, a type argument like T
can be a union. Indeed, you can call func()
with a T
equal to the full Types.ME | Types.YOU
union, like so:
func(
Math.random() < 0.999 ? Types.ME : Types.YOU,
{ hat: true }
) // compiles without error
If you inspect that, you'll see that T
is inferred as Types
(the full union of Types.ME | Types.YOU
, and therefore attr
is allowed to be {hat: true}
even in the 99.9% likely event that type
is Types.ME
.
There is a longstanding open feature request at microsoft/TypeScript#27808 which asks for a way to say "T
will be exactly one of Types.ME
or Types.YOU
; it cannot be a union". And then, maybe inside the function body, checking type === Types.ME
would allow T
itself to be constrained to Types.ME
, and things would work as expected. And presumably the call with Math.random() < 0.999
would be rejected.
But for now it's not part of the language.
You might consider taking the approach where instead of having func
be generic, you make it similar to an overloaded function, where it has one call signature per member of Types
. You can write that as a function with a rest parameter whose type is a discriminated union of tuple types, and the compiler will treat it as such inside the function body.
Perhaps like this:
type FuncArg =
[type: Types.ME, attr: { keys: true }] |
[type: Types.YOU, attr: { hat: true }];
const func: (...args: FuncArg) => void = (type, attr) => {
if (type === Types.ME) {
console.log(attr.keys) // this works
}
}
And now you can't make the invalid call:
func(
Math.random() < 0.999 ? Types.ME : Types.YOU,
{ hat: true }
) // error, type 'Types.ME' is not assignable to type 'Types.YOU'
Everything works because now type
and attr
are bound together as desired in FuncArg
; it's like imagining T
were constrained to be just one member of Types
at a time, and walking through the possibilities. Note that FuncArg
could, if necessary, be computed from the Attributes
type given in the question, or from another mapping interface, but that is out of scope for the question as asked.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论