Search code examples
typescripttypescript-generics

How to make generic callback function type work


The following example says it all:

function fn<R>(cb: <S1 extends string>(string: S1) => R) {
  return <S2 extends string>(str: S2) => cb(str)
}

const f = fn((passedString) => {
  return { passedString }
})

// I expect an error: This comparison appears to be unintentional ... but there's
// no error which means `passedString` is not known to be of type `""`
export const someValue = f("").passedString === "dfafa"

I would expect f("").passedString to be of type "", but it looks like the compiler couldn't pick the value of type S1 after the call to fn. Is there any way to get around this?

Update

I am aware that I can achieve the desired behaviour by placing the type parameter on the callback function itself passed to fn as also mentioned in @jesus Diaz Rivero answer below. However, all parameters to the callback must be explicitly typed in such case, which I'm trying to avoid.

function fn<R, V>(cb: (number: number, value: V) => R) {
  return (number: number, str: V) => cb(number, str)
}

// `number` must be typed explicitly
const f = fn(<V>(number: number, passedValue: V) => {
  return { number, passedValue }
})

export const someValue = f(8, "" as const).passedValue === "dfafa" // correctly errors

In this case, I would like the number parameter to not have to be explicitly typed when calling fn, but instead taken from the type signature of fn. In my case, the extra parameter (also the first parameter) is not as simple as number. If the callback were not generic, it would work, but with the introduction of <V>, it doesn't.

PS: The actual function I'm working with is a create method on a builder that takes advantage of the type info on the builder to specify the types of the parameters of the callback, and returns a middleware. The middleware is used as a function call. It looks something like:

export const builder = {
  // Rather than `number`, the actual type is based on type parameters on the builder
  // which I don't want caller to explicitly specify
  create<R, P extends unknown[]>(cb: (opts: number, ...params: P) => R) {
    return (...params: P) => {
      // Actual middleware
      return (opts: number) => cb(opts, ...params)
    }
  }
}

Solution

  • In order to let TS correctly infer the type you should the S1 generic parameter to be part of fn:

    function fn<R, S1 extends string>(cb: (input: S1) => R) {
      return cb
    }
    
    const f = <T extends string>(passedString: T) => {
      return { passedString }
    }
    
    // Non-callback version correctly throws error
    const someValue = f("").passedString === "da";
    // Callback version now throws error
    const someCallbackValue = fn(f)("").passedString === "da";
    

    Note I have simplified fn since it seems to be a bit redundant to return a function which returns the callback.