简体   繁体   中英

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

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.

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM