How to use overloading with multiple function signatures in TypeScript (overload signature is not compatible with its implementation signature)?

40 views Asked by At

I have this TypeScript playground, which demonstrates the error I'm getting right up top. It's a generic error message that's not very helpful:

This overload signature is not compatible with its implementation signature.(2394)
input.tsx(11, 23): The implementation signature is declared here.
function call(action: 'format', source: FormatRust): Promise<FormatOutput> (+1 overload)

Essentially, I have two functions, format and convert, which I wrap into a call function. In my production code, format has about 20 overload signatures (which all work), and convert has a similar number of overload signatures. Now I'm trying to combine them into the call function, but am getting a hard-to-make-sense-of error. The TypeScript code is pasted here too keep things colocated, but the TS playground shows in more detail the error.

export type Action = 'convert' | 'format'

export async function call<I extends ConvertInput['image']['input']>(
  action: 'convert',
  source: ConvertImageWithImageMagick<I>,
): Promise<ConvertOutput>
export async function call(
  action: 'format',
  source: FormatRust,
): Promise<FormatOutput>
export async function call<A extends Action>(action: A, source: any) {
  switch (action) {
    case 'convert':
      return await convert(source)
    case 'format':
      return await format(source)
  }
}

async function format(source: any) {
  // pretend do work...
  // return output file path:
  return {
    file: {
      path: 'bar'
    }
  }
}

async function convert(source: any) {
  // pretend do work...
  // return output file path:
  return {
    file: {
      path: 'foo'
    }
  }
}

export type FormatInputItem = {
  input: any
  extend: any
}

export type FormatInputWithExtension<T extends FormatInputItem> = {
  input: {
    format: T['input']
  }
} & FormatExtension<T['extend']>

export type FormatExtension<E extends { format: any }> = Omit<
  E,
  'format'
>

export type FormatOutput = {
  code: string
}

export type ConvertImageWithImageMagick<
  I extends ImageMagickInputFormat,
> = {
  input: {
    format: I
  }
  output: {
    format: Exclude<ConvertInput['image']['output'], I>
  }
} & ConvertExtension<ConvertInput['image']['extend']>

export type ConvertExtension<E extends { input: any; output: any }> =
  Omit<E, 'input' | 'output'> & {
    input: Omit<E['input'], 'format'>
    output: Omit<E['output'], 'format'>
  }

export type ConvertInput = {
  // ffmpeg: {
  //   input: FfmpegFormat
  //   output: FfmpegFormat
  //   extend: ConvertVideoWithFfmpegNodeInput
  // }
  // latex_to_png: {
  //   input: ConvertLatexToPngInputFormat
  //   output: ConvertLatexToPngOutputFormat
  //   extend: ConvertLatexToPngNodeInput
  // }
  image: {
    input: ImageMagickInputFormat
    output: ImageMagickOutputFormat
    extend: ConvertImageWithImageMagickNodeInput
  }
}

export type ConvertImageWithImageMagickNodeInput = {
  handle: 'external'
  input: {
    format: ImageMagickInputFormat
    file: {
      path: string
    }
  }
  output: {
    format: ImageMagickOutputFormat
    file?: {
      path: string
    }
  }
  pathScope?: string
  colorCount?: number
  density?: number
  quality?: number
}

export const IMAGE_MAGICK_INPUT_FORMAT = [
  'cr2',
  'gif',
  'png',
  'jpg'
] as const

export type ImageMagickInputFormat =
  (typeof IMAGE_MAGICK_INPUT_FORMAT)[number]
export const IMAGE_MAGICK_OUTPUT_FORMAT = [
  'png',
  'jpg'
] as const

export type ImageMagickOutputFormat =
  (typeof IMAGE_MAGICK_OUTPUT_FORMAT)[number]

export type ConvertOutput = {
  file: {
    path: string
  }
}

export type FormatRust = FormatInputWithExtension<FormatInput['rust']>

export type FormatInput = {
  // clang: {
  //   input: ClangFormat
  //   extend: FormatCodeWithClangFormatNodeInput
  // }
  // assembly: {
  //   input: 'asm'
  //   extend: FormatAssemblyNodeInput
  // }
  rust: {
    input: 'rust'
    extend: FormatRustNodeInput
  }
}

export type FormatRustNodeInput = {
  handle: 'external'
  format: string
  input: {
    file: {
      path: string
    }
  }
  output: {
    file?: {
      path: string
    }
  }
  pathScope?: string
}

How do I get the call function to support not only these 2 divergent function signatures (convert/format), but also 10+ more sub-functions, which also take a source property?

Here's a screenshot of my local code, which breaks at the first overload of call(action: 'format', source...). This just goes to show that the error seems to be occurring after 10+ convert overloads, but at the first format overload. Even if I delete all the call(action: 'convert') ones, it still errors on the format ones, any idea how to get both of these (and potentially other similar subfunctions like archive, inspect, etc., to work in TypeScript?

enter image description here

Function overloading seems to be the best solution so far to my generic problem, but now I'm running into this, so not sure if there's a way to accomplish this that would work better without function overloading. Anything to allow composing 10+ nested functions (each with 10+ overloaded signatures), into one single call function. Function overloading solved a bunch of problems I was facing before, so ideally we can keep it, but I'm open to removing/changing it.

The main question is, how can I get more of an error message than just the nondescript "overload signature is not compatible with its implementation signature"? How can I better debug this to find the cause of the error?

0

There are 0 answers