Kai Sellgren
Kai Sellgren

Reputation: 30192

How to narrow this type in TypeScript?

The following sample code does not pass the type check. I would like to find a way to make it pass without as casting, if possible.

type SupportedHandlerType = string | number | Date
type Handler<T> = (data: T[]) => void

function example<T extends SupportedHandlerType>(data: T[]) {
  const handler = getHandler(data)
  handler(data)
}

function stringHandler(data: string[]) {

}

function numberHandler(data: number[]) {
    
}

function dateHandler(data: Date[]) {
    
}

function getHandler<T>(data: T[]): Handler<T> {
    const first = data[0]
    if (typeof first == 'string') {
        return stringHandler // Type 'T' is not assignable to type 'string'
    }
    if (typeof first == 'number') {
        return numberHandler // another error here
    }
    return dateHandler // and here
}

My real life version is much more complicated, but this simple piece show cases the problem I have: I have a few types forming a union (SupportedHandlerType), and the generic function is called with one of those. I have a getHandler(), which dynamically looks at the types to determine which one it is and returns the appropriate handler function, which has a dedicated type instead of T.

Is there a way to somehow narrow the types so that I don't get errors such as Type 'T' is not assignable to type 'string'?

Playground

Upvotes: 0

Views: 121

Answers (2)

bvdb
bvdb

Reputation: 24710

Arrays lose their typing once they are transpiled to javascript. Meaning, that you can't be sure what the type of an array is, except if you iterate through them and check the value of each element.

That's why I would add these 2 functions.

function isArrayOfStrings(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === "string");
}

function isArrayOfNumbers(value: unknown): value is number[] {
  return Array.isArray(value) && value.every(item => typeof item === "number");
}

More importantly, notice the return type of these functions. These kind of functions are sometimes called "type-guards". the ... is ... return-value-type indicates that when the function returns true, this also means that we can conclude that the input variable was of a certain type.

Finally, I changed something to the T generic. You can make them extend something. By doing so, I simplified the generics and indicated that the input T should always be an array.

type Handler<T extends any[]> = (data: T) => void

function getHandler<T extends any[]>(data: T): Handler<T> {
  if (isArrayOfStrings(data)) {
    return stringHandler;
  }
  if (isArrayOfNumbers(data)) {
    return numberHandler;
  }
  return dateHandler;
}

Alternatively, if you don't want to loop through the entire arrays, you could simplify the isArrayOf... functions by just checking the type of the first element.

Additional Note:

Please note that the following code is in no way affected.

type SupportedHandlerType = string | number | Date
function example<T extends SupportedHandlerType>(data: T[]) {
  const handler = getHandler(data)
  handler(data)
}

The T in the function above is a different T than the one of the other functions, which I admit can be confusing. Let me try to clarify that:

  • data can be a string[] in the context of this function. Locally here T[] is string[] in that case.
  • At the same time data then also complies to the requirements of getHandler(data). Because string[] also complies to the T of getHandler, where T is string[].

So, there are 2 different T's at play. Or if that gets you confused, rename the T to U only for this part of the code.

type SupportedHandlerType = string | number | Date
function example<U extends SupportedHandlerType>(data: U[]) {
  const handler = getHandler(data)
  handler(data)
}

Upvotes: 2

Valerio Ageno
Valerio Ageno

Reputation: 134

The issue is related to how you are declaring the handler functions. You have to stick to the same structure declared in type Handler

type SupportedHandlerType = string | number | Date
type Handler<T> = (data: T[]) => void

function example<T extends SupportedHandlerType>(data: T[]) {
  const handler = getHandler(data)
  handler(data)
}

function stringHandler<T = string>(data: T[]) {

}

function numberHandler<T = number>(data: T[]) {
    
}

function dateHandler<T = Date>(data: T[]) {
    
}

function getHandler<T>(data: T[]): Handler<T> {
    const first = data[0]
    if (typeof data == 'string') {
        return stringHandler // Type 'T' is not assignable to type 'string'
    }
    if (typeof data == 'number') {
        return numberHandler // another error here
    }
    return dateHandler // and here
}

Upvotes: 1

Related Questions