如何在 TypeScript 中正确推断或声明带有混合参数的数组转换函数的类型?

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

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&lt;T&gt;(args: T) {
   return Array.isArray(args) ? args : [args];
}

Now, I have a (T &amp; 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&lt;T&gt;(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 &#39;(T &amp; any[]) | T[]&#39; is not assignable to type &#39;T extends any[] ? T : T[]&#39;.
  Type &#39;T &amp; any[]&#39; is not assignable to type &#39;T extends any[] ? T : T[]&#39;

What I am missing here ?

Playground link

Thanks !

答案1

得分: 1

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

The problem with

    function castToArray&lt;T&gt;(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 &amp; 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&#39;s not part of the language.

And it&#39;s not immediately obvious how to implement such a feature; it&#39;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&#39;s not valid; it&#39;s more like `any[] extends T`, since `T` could be some *supertype* of `any[]`.  But it wouldn&#39;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 &quot;there is some overlap between `any[]` and `T`&quot;. See [microsoft/TypeScript#33014](https://github.com/microsoft/TypeScript/issues/33014) for a similar discussion.

For now, if you want that to compile, you&#39;ll need to just tell the compiler not to worry about it with a type assertion:

    function castToArray&lt;T&gt;(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 &quot;reverse&quot; 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&lt;T&gt;(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&#39;t technically type safe, since `T` itself might be an array type, but the compiler allows it and I&#39;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&#39;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&#39;t be `T[]` if it&#39;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&lt;T&gt;(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 &amp; 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&lt;T&gt;(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&lt;T&gt;(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

huangapple
  • 本文由 发表于 2023年6月5日 22:18:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76407371.html
匿名

发表评论

匿名网友

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

确定