Reputation: 10918
I have a function that returns one item from an array:
function oneItem(arr: any[]) {
return arr[~~(Math.random() * arr.length)];
}
I'd like to have approriate types for this.
What I've Tried:
type ValuesOf<T extends any[]>= T[number];
function oneItem<ARRAY extends any[]>(arr: ARRAY): ValuesOf<ARRAY> { ... }
const a = oneItem(["foo", "bar"]);
// typeof a is (string) instead of ("foo" | "bar")
Found here (https://github.com/Microsoft/TypeScript/issues/20965)
How do I return a single item from an array with proper type?
EDIT: I figured it out for readonly arrays:
type ValuesOf<T extends readonly any[]>= T[number];
function oneItem<ARRAY extends readonly any[]>(arr: ARRAY): ValuesOf<ARRAY> { ... }
const a = oneItem(["foo", "bar"] as const);
Upvotes: 6
Views: 4700
Reputation: 327799
When you say "proper" type, I guess you mean you want the narrowest possible type the compiler can infer.
Whether or not this is "proper" is in the eye of the beholder; the runtime code let x = 0
could have many possible typings in TypeScript, and the compiler has to pick just one of them unless you annotate the type yourself.
For example, I might want it to be interpreted as let x: number = 0
which is the default compiler behavior and which makes sense if I plan to assign different numbers to x
later. Or I might want let x: 0 | 1 = 0
because I'm only going to write code like x = y & 1
and I know that only those values will come out. Or I might want let x: 0 = 0
because x
will always be 0
(although presumably I would use const
in that case). Or maybe I want let x: number | string = 0
because sometimes I will assign string
values to it. Or maybe I want any other crazy type that includes 0
as a value.
If you don't annotate it yourself, then the compiler has to infer the type, so it makes an assumption: non-readonly
and non-const
values will tend to be inferred as a "widened" type like string
, number
, or boolean
, while readonly
and const
values will tend to be inferred as a "narrow" literal type like "foo
", 0
, or true
.
Sometimes the compiler's default assumption won't match the intent of the developer. That doesn't mean the compiler did anything wrong; it's not as if it takes let x = 0
and infers string
for x
. It just means the developer might be required to put in some more effort to communicate his or her intent.
So, when I write ["foo", "bar"]
, the compiler tends to infer that as string[]
. If I want something else, I can annotate it myself. Let's start with this version of your function:
function oneItem<T>(arr: readonly T[]): T {
return arr[~~(Math.random() * arr.length)];
}
Here are some outputs:
let arr1 = ["foo", "bar"]; // inferred as string[]
const res1 = oneItem(arr1); // string
let arr2: Array<"foo" | "bar"> = ["foo", "bar"];
const res2 = oneItem(arr2); // "foo" | "bar"
let arr3: ["foo", "bar"] = ["foo", "bar"];
const res3 = oneItem(arr3); // "foo" | "bar"
let arr4: readonly ["foo", "bar"] = ["foo", "bar"];
const res4 = oneItem(arr4); // "foo" | "bar"
let arr5 = ["foo", "bar"] as const; // inferred as readonly ["foo", "bar"];
const res5 = oneItem(arr5);
You can see that arr1
uses the default string[]
inferred type, and thus string
comes out of the function. For arr2
, arr3
, and arr4
, I've annotated the arrays as progressively narrower types, and all of them allow "foo" | "bar"
to come out of the function. The last one, arr5
, uses a const
assertion to ask the compiler to infer the type as the narrowest value it can, corresponding to that of arr4
.
So one way for you to proceed is just to specify the value's type more narrowly.
Of course, in your use case, you have something like
const resArrayLiteral = oneItem(["foo", "bar"]); // string
And you would like "foo" | "bar"
to come out instead of string
. Why should the caller of oneItem()
have to ask the compiler to narrow the type of the array literal ["foo", "bar"]
? Can't the implementer of oneItem()
use some sort of contextual typing hint to tell the compiler to interpret things more narrowly?
Well, kind of, yes. It's just not pretty. Here's one way to do it:
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};
function oneItem<T extends Narrowable>(arr: readonly T[]): T {
return arr[~~(Math.random() * arr.length)];
}
Here we are giving T
a constraint to Narrowable
, a union type of a bunch of things including string
, number
, and boolean
. In fact, the Narrowable
type is a kind of tedious version of unknown
, since most anything should be assignable to it. But Narrowable
gives the compiler a hint (see microsoft/TypeScript#10676) that you'd like to see T
inferred as a literal type, if possible.
Let's see if it works:
const resArrayLiteral = oneItem(["foo", "bar"]); // "foo" | "bar"
Looks good now! T
is inferred as "foo" | "bar"
and not string
.
As I said, though: it's not pretty. There's an open issue, (microsoft/TypeScript#30680), suggesting that we be able to write the oneItem()
signature as something like
function oneI<const T>(arr: readonly T[]): T; // currently invalid syntax
where the const
is in the signature, and it behaves similarly to having the caller use as const
. Not sure if that issue will ever go anywhere, though. For now, we have to jump through hoops like Narrowable
.
Also, please note that the above T extends Narrowable
does nothing to change the behavior of arr1
. If you write let arr1 = ["foo", "bar"]
, then arr
will be inferred as string[]
. Once it's a string[]
, the compiler has forgotten all about "foo"
and "bar"
as literal types. And thus oneItem(arr1)
will return string
; T
will be inferred as string
. So at some point you might find yourself needing to write narrow types yourself, if you want them.
Okay, hope that helps; good luck!
Upvotes: 4