1

I am trying to implement a variadiac argument for a generic function such that when the generic type is undefined, no argument is required, otherwise a single argument is matching the specified type.

For example

const a = variadiacFunc(); // ok
const b = variadicFunc<string>('A string') // ok
const c = variadicFunc<string>() // error Expected 1 arguments, but got 0; Arguments for the rest parameter 'context' were not provided.
const d = variadicFunc<string | undefined>() // ok
const e = variadicFunc<string | undefined>('Another string') // ok

I am implementing this using conditional tuple types

type ConditionalTupleArg<T> = [T] extends [undefined] ? [] : [T];
const variadicFunc = <T = undefined>(...context: ConditionalTupleArg<T>) {...};

Inspecting the types in TS playground, this seems to do exactly what I want

export type OptionalArgTuple<T> = [T] extends [undefined] ? [] : [T]

type UndefinedOnly = OptionalArgTuple<undefined>; // []
type StringOrUndefined = OptionalArgTuple<string | undefined>; // [string | undefined]
type StringOnly = OptionalArgTuple<string>; // [string]

In my local create-react-app project I am seeing an error because StringOrUndefined is resolving to [string] rather than [string | undefined] which is causing compile errors for invocation involving union with undefined in the case of d above.

I'm not sure why this is happening, if there are tsconfig options which control or influence this behavior which may be configured incorrectly in my local project.

The project is currently configured to use typescript 4.7.4, and my tsconfig.json is

{
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "target": "ES6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "downlevelIteration": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "module": "CommonJS",
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "typeRoots": [
      "./node_modules/@types",
      "../../node_modules/@types",
      "./src/types"
    ]
  }
}

UPDATE: explicitly running tsc --v locally suggests that the code is building with 4.5.4 and not 4.7.4 as is listed in my package.json, however, using v4.5.4 in TS playground the inferred types are still what I am expecting.

UPDATE 2: I set typescript.tsdk in my vscode settings.json to point to the correct typescript version and the error displayed in the editor has now changed

variadicFunc<string | undefined>(); // Expected 1 arguments, but got 0; Arguments for the rest parameter 'ctx' were not provided.

Allowing the types to be distributed resolves the error:

export type ConditionalTupleArg<T> = T extends undefined ? [] : [T];

variadicFunc<string | undefined>(); // ok
variadicFunc<string | undefined>('a string'); // ok

So I think the issue my local build using an old (4.5.4) version of typescript.

rcbevans
  • 7,101
  • 4
  • 30
  • 46
  • 3
    Enable `strictNullChecks` (or even better, `strict`) – kelsny Oct 19 '22 at 21:50
  • @caTS I applied those, as well as all other options specified in the tsconfig of TS playground and same error. – rcbevans Oct 19 '22 at 22:10
  • For some reason my colleagues using MacOS the code I'm having issues with compiles just fine so I think something was b0rked in my local environment. Setting `strict` in my tsconfig and fixing the version of typescript being used resolved the issue. – rcbevans Oct 20 '22 at 19:06

1 Answers1

1

So this small adjustment is fixing your problem.

export type OptionalArgTuple<T> = T extends undefined ? [] : [T]

type UndefinedOnly = OptionalArgTuple<undefined>; // []
type StringOrUndefined = OptionalArgTuple<string | undefined>; // [string | undefined]
type StringOnly = OptionalArgTuple<string>; // [string]

const variadicFunc = <T = undefined>(...context: OptionalArgTuple<T>)=>{throw new Error("not implemented")};
const a = variadicFunc<undefined>()
const b = variadicFunc<undefined|string>() //ok
const c = variadicFunc<string>("a")

So error is caused by the type export type OptionalArgTuple<T> = T extends [undefined] ? [] : [T]. By using [undefined] typescript won't distribute your union type. That means typescript will create ONE function with this signature

function variadicFunc<undefined|string>("as") \\ type variadicFunc = (...args:[undefined|string])=> unknown

and this function will always required at least one argument.

What you actually want to achieve is this:

variadicFunc<undefined|string>("as") \\ type variadicFunc = (...args:[])=> unknown | (...args:[string] )=> unknown

And you will achieve this by removing the brackets around undefined and distribute your union type. playground

here is a link to an stackoverflow post that will provide more information on this topic

Filly
  • 411
  • 2
  • 8
  • That's I'd already found this and made the change independently, however the underlying issue was that the `| undefined` is being stripped out of the type leaving only `[string]` in either case. – rcbevans Oct 20 '22 at 19:05