Make a function parameter required in TypeScript

Viewed 2255

In the following code:

type NoArg = {
  (): void
}

type OneArg = {
  (x:number): void
}

let noArg: NoArg = (x:number)=>{}
let oneArg: OneArg = ()=>{}

Only the first assigned generates a compiler error. I understand why this is the case, because JavaScript allows functions to be passed with less than their complete set of possible arguments, and that’s different than saying the arguments are optional which has to do with how the function is called rather than how it’s passed. See the FAQ.

But that said, is there any way to construct a version of the OneArg interface that will not be compatible with a zero argument function?

I understand this can be done via branding or nominal typing, e.g.,

type OneArg = {
  (x:number): void
  _brand: “OneArg”
}

But that, or any other type of nominal typing solution, requires doing extra work on assignment (e.g., you have to explicitly add the _brand property to the function).

So my question is—is there any way to construct a type NoArg that will fail the simple assignment let oneArg: OneArg = ()=>{}?

The FAQ linked above says “There is currently not a way in TypeScript to indicate that a callback parameter must be present.“ Does that completely rule out what I want to do here? I’m hoping it doesn’t since this isn’t a callback parameter, but perhaps the rationale is the same.

UPDATE: In the comments below, a question was raised as to whether this could be achieved with a type guard test. The answer, as far as I can tell, is no, because the type overlap means the type guard won't narrow the types. You can see it in this playground.

1 Answers

After some discussion with @aluan-haddad, I have a partial solution. I can't get exactly what I asked for, i.e., an "automatic" type definition that distinguishes between a function with the required parameter and one without, but I have come up with a "discriminator" function that will distinguish between the two functions and apply an appropriate branded type.

The solution relies on using a conditional type, coupled with the fact that the one-argument function is incompatible with the zero-argument type (even though the reverse is not true), to coerce the correct types.

type NoArg = {
    _brand: 'NoArg'
    ():void
}

type OneArg = {
  _brand: 'OneArg'
  (x:number): void
}

// Correct typings
let noArg: NoArg = discriminator(()=>{})
let oneArg: OneArg = discriminator((x:number)=>{})

// Both of these error, as hoped!
let noArgError: NoArg = discriminator((x:number)=>{})
let oneArgError: OneArg = discriminator(()=>{})

function discriminator<T extends (x:any)=>any>(myFunc: T) {
    let discriminatedFunc
    if(myFunc.length === 0) {
        discriminatedFunc = {
            _brand: 'NoArg',
            myFunc
        }
    } 
    else {
        discriminatedFunc = {
            _brand: 'OneArg',
            myFunc
        }
    }
    return discriminatedFunc as unknown as T extends ()=>any ? NoArg : OneArg
}

And on a playground.

NB: To my point that this is relying on the one argument function being incompatible with the zero type function, note that the reverse version of the conditional type T extends (x:number)=>any ? OneArg : NoArg will not work, because it always resolves to OneArg.

Now, is this better than "branding" the functions in the first place via constructor functions? I think so, because you only need one discriminator function vs two branding functions. And also because you don't necessarily need to know the signatures of your functions ex-ante, i.e., when you pass them to the discriminator.

Anyone see any problems with this solution? Is there a better one, or is this all TS permits?

Related