typescriptoverloadingnarrowing

Is there a way to narrow typescript overloads with optional parameters?


I'm trying to do the following:

interface RgbColor {
  r: number;
  g: number;
  b: number;
}

function rgbToHex(r: RgbColor, g?: undefined, b?: undefined): string
function rgbToHex(r: number, g: number, b: number): string
function rgbToHex(r: number|RgbColor, g?: number, b?: number): string {
  if (r instanceof Object) {
    // must be RgbColor
    g = r.g;
    b = r.b;
    r = r.r;
  }

  // A) r was an RgbColor and we've already set r, g, and b to numbers in the if block
  // B) we're meeting the second overload and r, g, and b are all numbers
  // either way we know they are all numbers now

  let rHex = r.toString(16);
  let gHex = g.toString(16); // ERROR: Object is possibly 'undefined'.
  let bHex = b.toString(16); // ERROR: Object is possibly 'undefined'.

  if (rHex.length == 1) rHex = "0" + rHex;
  if (gHex.length == 1) gHex = "0" + gHex;
  if (bHex.length == 1) bHex = "0" + bHex;

  return "#" + rHex + gHex + bHex;
}

I'm getting the errors on the lines indicated by ERROR comments.

From a javascript perspective the function is fine:

function rgbToHex(r , g, b) {
  if (r instanceof Object) {
    g = r.g; 
    b = r.b; 
    r = r.r;
  }

  let rHex = r.toString(16);
  let gHex = g.toString(16);
  let bHex = b.toString(16);

  if (rHex.length == 1) rHex = "0" + rHex;
  if (gHex.length == 1) gHex = "0" + gHex;
  if (bHex.length == 1) bHex = "0" + bHex;

  return "#" + rHex + gHex + bHex;
}

rgbToHex({ r: 120, g: 50, b: 5 }) // "#783205"
rgbToHex(120, 50, 5) // "#783205"

My question is how can I setup the function signatures such that this just works without having to do any casting and without setting up any redundant variables in the function body. i.e. I want to avoid doing let gVal: number = g || (r as any).g;.

The other alternative would be to split this into two function and have one call the other; but I wouldn't do that with javascript, so it seems wrong that I'd have to do that with typescript.

TS Playground Link

Sorry if this is a duplicate. I spent a couple of hours looking through similar questions but I couldn't find anything that addressed this issue.


Solution

  • After a bit more googling, reading of other SO questions, and flicking through the typescript github issues, it looks like this is a yet-to-be-implemented feature within typescript.

    The overload narrowing that works outside of a function does not yet apply within a function:

    // function definition for swapping between number and string types
    function foo(bar: string): number // overload 1
    function foo(bar: number): string // overload 2
    function foo(bar: number | string): number|string 
    { ... }
    
    const a = foo('string');
    a.toPrecision(); // TS knows a is a number, so this has no errors
    
    const b = foo(5);
    b.toLowerCase(); // TS knows b is a string, so this has no errors
    
    const c = Math.random() < .5 ? a : b; // TS knows c as string | number
    
    // the actual implementation of an overloaded method does not count as 
    // one of the possible overloads, so calling foo with a variable of type (string|number)
    // is not valid
    const d = foo(c); // error on parameter 'c': No overload matches this call.
    

    So that's all as expected. But when we try to implement foo we see the same exclusionary logic is not applied within the method:

    function foo(bar: string): number // overload 1
    function foo(bar: number): string // overload 2
    function foo(bar: number | string): number | string {
      return bar; // this is valid, it clearly should not be
    }
    

    It looks like the typescript team have not yet made the code-analysis within a function aware of the overloads of that function, it seems like strictly the signature of the implementation is enforced. This is obviously wrong.

    QED: Error in tooling. Wait for fix.