英文:
How to correctly infer or declare the type of a cast to array function with typescript with mixed arguments?
问题
function castToArray<T>(args: T): T extends any[] ? T : T[] {
return Array.isArray(args) ? args : [args];
}
英文:
I want to have a really simple tool to cast a value to an array if needed. Like this :
function castToArray(args: unknown) {
return Array.isArray(args) ? args : [args];
}
So far so good.
Ideally, I'm expecting typescript to infer the type of the array from the args
type. But without no more burden, TS is logically typing output as any[]
.
To solve this, I've tried to used a naive approach with generics :
function castToArray<T>(args: T) {
return Array.isArray(args) ? args : [args];
}
Now, I have a (T & any[]) | T[]
return type but a single item is sadly still be treated as any
type.
I then tried to define a conditional return type like so :
function castToArray<T>(args: T): T extends any[] ? T : T[] {
return Array.isArray(args) ? args : [args];
}
It works great to type output except that TS is now complaining about invalid assignation :
Type '(T & any[]) | T[]' is not assignable to type 'T extends any[] ? T : T[]'.
Type 'T & any[]' is not assignable to type 'T extends any[] ? T : T[]'
What I am missing here ?
Thanks !
答案1
得分: 1
以下是您要翻译的代码部分:
The problem with
function castToArray<T>(args: T): T extends any[] ? T : T[] {
return Array.isArray(args) ? args : [args];
}
is that TypeScript is currently (up to and including TS5.1) unable to use [control flow analysis](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#control-flow-analysis) to affect [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) *type parameters* like `T`. When you check `Array.isArray(args)`, that acts as a [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) on `args` (so `args` can be narrowed from `T` to something like `T & any[]`), but it has no effect whatsoever on `T`. The compiler really has no idea what sorts of values might actually be assignable to a generic [conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) like `T extends any[] ? T : T[]`. So it gives up and complains.
There is an open feature request at [microsoft/TypeScript#33912](https://github.com/microsoft/TypeScript/issues/33912) asking for some fix to this that would allow you to implement a generic function returning a conditional type without [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions). For now, though it's not part of the language.
And it's not immediately obvious how to implement such a feature; it's hard to describe what effect `Array.isArray(args)` should have on `T`. You might expect that `T` could be re-[constrained](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints) to `T extends any[]`, but that's not valid; it's more like `any[] extends T`, since `T` could be some *supertype* of `any[]`. But it wouldn't be valid to constrain `T` from below like `any[] extends T` *either*, because `T` could indeed also be a *subtype* of `any[]`. The check does something to `T` like "there is some overlap between `any[]` and `T`". See [microsoft/TypeScript#33014](https://github.com/microsoft/TypeScript/issues/33014) for a similar discussion.
For now, if you want that to compile, you'll need to just tell the compiler not to worry about it with a type assertion:
function castToArray<T>(args: T) {
return (Array.isArray(args) ? args : [args]) as (
T extends any[] ? T : T[]);
}
----
Instead the current way to implement something like this without assertions is to "reverse" the constraint and think of `T` as the element type of the array no matter what, and accept an `args` which might or might not be `T` or an array of `T`:
function castToArray<T>(args: T | T[]): T[] {
return Array.isArray(args) ? args : [args];
}
Now we are using the fact that the type guard acts on `args` and not `T`. If `Array.isArray(args)` returns `true` then the compiler concludes that `args` is of type `T[]` (this isn't technically type safe, since `T` itself might be an array type, but the compiler allows it and I'm not going to complain. Just be careful if you find yourself passing in arrays-of-arrays. I think it usually behaves properly, but it's something to watch). And if it returns `false` then the compiler concludes that `args` is of type `T` (this *is* type safe, since it can't be `T[]` if it's not an array). And thus the returned value is observably of type `T[]` to the compiler, and it compiles without error.
---
Either one of those should work, and it depends on your use cases which of those, if any, are better for your purposes.
[Playground link to code](https://www.typescriptlang.org/play?#code/HYQwtgpgzgDiDGEAEBBKUICcAuBhA9sACYCW2JhIANkgN4CwAUE0q0gGYCuw85hS8EFGwAVfCkyYQATwA8IgHwAKEJgDmUAFxIRASjos2RzBGydMwJEolTpAOhJQbMleqj6A-ElUak2gNo+UAC6+kJWhkZROkgQAB7yEMRQ3sDS-sFIXiJ+Ohm6ANyRSAC+TMVUpt6SAMxIALwCQqLiki4AjIUVVQBu1HWNqpg1-gAMwUXMjEaV2NWYACwNTcJiztJK-u2hkzO91EuDkgtjE+WMZVOgkLAIyABKEOwI2PiYYgCqwBSWDFNGXB4fEsglWrVs8mUQW0OQAPnlQjCMgZptEkCYzBZUG17I51q4NJ5qr4AkEzqjWJdunMhgMVi18Z1dmxZkg+lQ6bTTpNqfNDvS1jjNtsuhSkKz2fyhidxjypiUgA)
英文:
The problem with
function castToArray<T>(args: T): T extends any[] ? T : T[] {
return Array.isArray(args) ? args : [args];
}
is that TypeScript is currently (up to and including TS5.1) unable to use control flow analysis to affect generic type parameters like T
. When you check Array.isArray(args)
, that acts as a type guard on args
(so args
can be narrowed from T
to something like T & any[]
), but it has no effect whatsoever on T
. The compiler really has no idea what sorts of values might actually be assignable to a generic conditional type like T extends any[] ? T : T[]
. So it gives up and complains.
There is an open feature request at microsoft/TypeScript#33912 asking for some fix to this that would allow you to implement a generic function returning a conditional type without type assertions. For now, though it's not part of the language.
And it's not immediately obvious how to implement such a feature; it's hard to describe what effect Array.isArray(args)
should have on T
. You might expect that T
could be re-constrained to T extends any[]
, but that's not valid; it's more like any[] extends T
, since T
could be some supertype of any[]
. But it wouldn't be valid to constrain T
from below like any[] extends T
either, because T
could indeed also be a subtype of any[]
. The check does something to T
like "there is some overlap between any[]
and T
". See microsoft/TypeScript#33014 for a similar discussion.
For now, if you want that to compile, you'll need to just tell the compiler not to worry about it with a type assertion:
function castToArray<T>(args: T) {
return (Array.isArray(args) ? args : [args]) as (
T extends any[] ? T : T[]);
}
Instead the current way to implement something like this without assertions is to "reverse" the constraint and think of T
as the element type of the array no matter what, and accept an args
which might or might not be T
or an array of T
:
function castToArray<T>(args: T | T[]): T[] {
return Array.isArray(args) ? args : [args];
}
Now we are using the fact that the type guard acts on args
and not T
. If Array.isArray(args)
returns true
then the compiler concludes that args
is of type T[]
(this isn't technically type safe, since T
itself might be an array type, but the compiler allows it and I'm not going to complain. Just be careful if you find yourself passing in arrays-of-arrays. I think it usually behaves properly, but it's something to watch). And if it returns false
then the compiler concludes that args
is of type T
(this is type safe, since it can't be T[]
if it's not an array). And thus the returned value is observably of type T[]
to the compiler, and it compiles without error.
Either one of those should work, and it depends on your use cases which of those, if any, are better for your purposes.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论