Issue
I am working on a module where I want to let users define a list of "allowable types", to be used in other methods, but I'm struggling to make this work properly with typescript:
function initSomething<T extends ArrayLike<string>>(eventTypes:T) {
type EventType = T[number];
function doTheThing(type:EventType) {
console.log(type, eventTypes);
}
return { doTheThing, eventTypes };
}
// I'd like this to throw an error: 'potato' is not one of ['hello', 'bye']
initSomething(['hello', 'bye']).doTheThing('potato');
I know the trick that casts an array of strings into a const to convert it into a string litteral union type:
const eventTypes = ['hello', 'bye'] as const
type EventType = typeof eventTypes[number]
but I can't figure out how to adapt this to my case, where the array is a generic
I have managed to make something work, but it seems weird: I use keyof
to make T
a map of <eventType, any>.
function initSomething<T>(eventTypes:(keyof T)[]) {
type EventType = keyof T;
function doTheThing(type:EventType) {
console.log(type, eventTypes);
}
return { doTheThing, eventTypes };
}
// Getting the error below, as expected. Perfect!
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.
initSomething(['hello', 'bye']).doTheThing('potato');
Is there a way to do the same thing without this weird keyof
trick, and making a simple union of string literals from the array instead ?
Solution
The problem with your code is that TypeScript generally infers that an array literal like ['hello', 'bye']
has the type string[]
. Often that's the type people want, such as in this code:
const arr = ['hello', 'bye'];
// ^? const arr: string[]
arr.push('howdy'); // okay
It would be pretty annoying if every time you initialized a string array the compiler assumed that it could only contain the exact literal values you put into it, in their exact order. But sometimes you do want the compiler to assume that, such as in your case (well, at least the literal values, if not the order).
As you noticed, you could use a const
assertion (the term "cast" is discouraged here) to get that behavior:
const arr = ['hello', 'bye'] as const;
// const arr: readonly ["hello", "bye"]
arr.push("howdy"); // error
and indeed you can use that with your version of initSomething()
:
initSomething(['hello', 'bye'] as const).doTheThing('potato'); // error
// -----------------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.
But you might not want the caller to need to remember to do that.
One approach is to use a const
type parameter in your function, like this:
function initSomething<const EventTypes extends ArrayLike<string>>(eventTypes: EventTypes) {
// ^^^^^
type EventType = EventTypes[number];
function doTheThing(type: EventType) {
console.log(type, eventTypes);
}
return { doTheThing, eventTypes };
}
This tells the compiler to infer EventTypes
more or less as if the caller had specified as const
. You can see that it works as expected:
initSomething(['hello', 'bye']).doTheThing('potato');
// --------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.
Still, at least with this example, you might not need this complexity. It doesn't seem like you really care about the EventTypes
type parameter, just the EventType
you compute from it. In this case you might as well make your function generic in EventType
directly:
function initSomething<EventType extends string>(eventTypes: ArrayLike<EventType>) {
function doTheThing(type: EventType) {
console.log(type, eventTypes);
}
return { doTheThing, eventTypes };
}
initSomething(['hello', 'bye']).doTheThing('potato');
// --------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.
That works because when a type parameter is constrained to string
, the compiler assumes that you care about its string literal type (or union of such types). That is, extends string
gives the compiler a context in which literal types will be preserved.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.