23

Is it possible in to define a function type and extend its argument list in another type (overloading function type?)?

Let's say I have this type:

type BaseFunc = (a: string) => Promise<string>

I want to define another type with one additional argument (b: number) and the same return value.

If at some point in the future BaseFunc adds or changes arguments this should also be reflected in my overloaded function type.

2
  • At the beginning of the parameter list or at the end ? At the start is the simple option. Commented Sep 12, 2018 at 11:15
  • @Titian Either would be fine though I'd prefer the start. Commented Sep 12, 2018 at 11:16

8 Answers 8

35

You can use Tuples in rest parameters and spread expressions together with conditional type and the inference behavior of conditional types to extract the parameters from the signature and reconstruct the new signature.

type BaseFunc = (a: string) => Promise<string>

type BaseWithB  = BaseFunc extends (...a: infer U) => infer R ? (b: number, ...a:U) => R: never;
Sign up to request clarification or add additional context in comments.

9 Comments

Thanks for helping out though as I wrote above I'll go with the "options" solution since it seems cleaner and more flexible.
@Thomas sure no problem, I agree that is a better design. I was just answering the specific typescript question you asked :)
@TitianCernicova-Dragomir thanks a lot for your answer. That is exactly what I need, because I need type definitions for already existing 3rd party library. Therefore changing function signature is not possible.
@MoshFeu in ts 4.0+ you can do type BaseWithB = BaseFunc extends (...a: infer U) => infer R ? (...a:[...U, number]) => R: never;
I found out that now Parameters<> returns Labeled Tuple Elements so you can use that to extend typescriptlang.org/play?#code/…
|
11

UPD: it's possible to extend a function's parameters in TypeScript. See Titian's answer.

Original answer: I doubt that it can be done in the way you described. However, you can try something like this:

interface BaseOptions {
    a: string;
}
type BaseFunc = (options: BaseOptions) => Promise<string>

interface DerivedOptions implements BaseOptions {
    b: number;
} 
type DerivedFunc = (options: DerivedOptions) => Promise<string>

Another advantage of this approach is that you have named parameters for free. So it's going to be cleaner from a caller side than just calling BaseFunc or DerivedFunc with positional arguments. Just compare:

someFuncA(1, undefined, true);

// vs

someFuncB({nofiles: 1, enableLogging: true});  // and bar: undefined is just omitted

2 Comments

Never doubt Typescript ;). But your solution is arguably cleaner then what is requested.
Thanks guys! I also think this solution is preferable. It also allows me to construct the options object in other scenarios. Something I would be blocked from doing with the type directly. Marking this as best answer.
11

You could use spread with Parameters<>:

function base(a: number, b: number, c: number): number
function derived(add: string, ...base: Parameters<typeof base>): number

The restriction is that base parameters have to be on the end.

2 Comments

great solution! :thumbsup:
And the return type can also be replaced with ReturnType<base>.
0

You can make any additional arguments optional.

class Plant {
  grow(inches: number) {}
}

class Grass extends Plant {
  // adding new optional argument in override
  grow(inches:number, cutOff?: boolean) {}
}

class CrabGrass extends Grass {
  // At this level of overriding, you no longer need it to be optional
  grow(inches:number, cutOff: boolean) {}
}

Comments

0

So, for those that ended up on this page just looking to quickly add another definition to your function interface:

Intersection type in Typescript

A & in TS in the context of a types means an intersection type. It merges all properties of 2 object types together and creates a new type

Example:

const SomeComponent: React.FC<Omit<ISomeInterface, 'arg1' | 'arg2' | 'arg3'>
 & { oneMoreArgument: string; }> = ({ arg1, arg2, arg3, oneMoreArgument }) => {

... function stuff

}

4 Comments

I don't think this is the same problem as the one stated in the question. React.FC<T> extends the prop interface which is not equivalent to extending the raw arguments list of a function. You can easily extend the prop interface separately and then simply use that as the generic. I'd argue it's easier to read as well. In the example you are simply manipulating an interface (omit + add) and then using it as the type of a single argument not actually modifying the top-level arguments.
Yeah, I was trying to show how to modify the type of a single argument. So, if you have a function that has a pre-defined interface for it's arguments, but, you want to modify it to have one more definition for a particular use case, instead of creating a new function interface. The React.FC was just an example.
@Thomas It would be awesome to get an example on how to simplify this as you describe.
no problem :) I usually don't use React.FC anymore as it adds things like children which I may not need. Functional components in react are basically any function that returns something react can render which means there is no need to decorate it any further than that. Something like this: gist.github.com/rudfoss/30615a4e8b99dece4a387b9b2b854973 is very close to what I usually use.
0

We can make use of the powerful utility types, so we don't even have to parametrize the initial function parameters ourselves, as this can be particularly problematic and time-consuming in more complex types.

I'll showcase a real-life scenario where I recently had to make use of this pattern. Consider the following function, which was opened by a click of <button>

openPopup = (event, isNotif) => { /*...*/ }
  • Now issue is, we can't manually type event: MouseEvent<HTMLButtonElement> because the interface MouseEvent does not accept any generics, producing a error.

  • We can however type our function as MouseEventHandler<HTMLButtonElement>

  • This however still does not solve our issue, as MouseEventHandler does not expect a custom second argument, yet another error.

We can however create a derived function handler in the following way:

type PopupHandler = (
  event: Parameters<MouseEventHandler<HTMLButtonElement>>[0]
  isNotif: boolean
) => ReturnType<MouseEventHandler<HTMLButtonElemement>>

// now it works!
openPopup: PopupHandler = (event, isNotif) => { /*...*/ }
  • We use Parameters utility type, to infer the event type as the first argument out of the args array
  • We use ReturnType to keep the proper type of return type.

This way we can actually extend (overload) an existing function type with our custom parameter while retaining the return type and maintaining proper types all throughout.

Comments

0

I know this was asked like 5 years ago but I decided to explore how I might solve this if I needed to do this today. I was able to achieve what you are asking for with this approach which is broken down for conceptual purposes:

const baseFunction = (foo: string) => 
    new Promise<string>(
        (resolve) => resolve(foo)
    )

type BaseFunctionType = typeof baseFunction
type AdditionalFunctionPropTypes = [
    bar: string
]


type CombinedParams = [
    ...Parameters<BaseFunctionType>, 
    ...AdditionalFunctionPropTypes
]

type DerivedFunction = (...combinedParams:CombinedParams) => ReturnType<BaseFunctionType>

Note - As of this writing (TSv5.4), there is currently no way to access the names of the function parameters. However, you can get their types via the Parameters<...> utility type. This allows for the preservation of their signatures but not their names.

As an aside, the Parameters<...> utility type does produce a labeled tuple with parameter names in it but these labels are purely aesthetic and can't currently be accessed. See this SO question and this GitHub issue for more info on that.

Taking the implementation above a bit further, it can be condensed down into a generic type like so:

type GenericDerivedFunction<BaseFunctionType extends (...args: any) => any, AdditionalFunctionPropTypes extends any[]> = 
    (...combinedParams:[...Parameters<BaseFunctionType>, ...AdditionalFunctionPropTypes]) => ReturnType<BaseFunctionType>

Then, you can type a new function as follows:

const baseFunction = (foo: string) =>
    new Promise<string>(
        (resolve) => resolve(foo)
    )

const derivedFunction: GenericDerivedFunction<typeof baseFunction, [bar: string]> =
    (foo, bar) => new Promise((resolve) => resolve(`${foo} ${bar}`))

If you wanted to refine the generic even slightly more to get rid of some of the any assertions and make it more concise, you can refactor using infer:

type ExtendFn<BaseFnT, AddPT extends any[]> = BaseFnT extends (...a: infer P) => infer R
    ? (...a: [...P, ...AddPT]) => R
    : never

Caveat - As previously noted, this does not enforce the preservation of property names, only their signatures. So, the derivedFunction here could be instantiated with (other, names) which would pass the type checks and simply assign the derived types to those parameters in order. However, the type checker will still complain if too many parameters are supplied until the typing definition is adjusted. Note that the code hint will still display the property names though.

What a great little exercise to completely kill my productivity for things I should actually have been working on instead 🤣

Comments

0

This is using Titian's correct answer.

I also needed to preserve properties, so with the idea from Titans answer, I made utility function for easy use.

/**
 * Returns an extended function with added parameters.
 * Initial parameters and return type is preserved, so is additional properties. 
 * Use labeled tuple elements for P (E.g: `[start: number, end: number]`)
 */
type AddParametersToFunction<F extends Function, P extends unknown[]> = F extends (...a: infer FP) => infer R ? (((...a:[...FP, ...P]) => R) & Pick<F, keyof F>): never;

Example usage:

From question:

type BaseFunc = (a: string) => Promise<string>
type ExtendedBaseFunc = AddParametersToFunction<BaseFunc, [b: number]>;

With properties:

type BaseFunc = {
  __type__?: string
  (a: string): number
}
type ExtendedBaseFunc = AddParametersToFunction<BaseFunc, [b: number, c: string]>;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.