Search code examples
typescriptgenericsoverloading

Typescript modify type based on generic


Question

Is there a way for the generic of a type to modify the type itself?
Can it be done without creating two functions?
This is not about js functionality, only about getting the types to match.

Example

When I call a function with the generic HTMLInputElement, I would like the return type to be Array<HTMLInputElement>.
When I call it with HTMLSelectElement it should be Array<HTMLSelectElement>.
But, when I call it with never or don't supply a generic, it should be string.

Minified example, that doesn't work since the return type is always Array<T> | string (T has the right value though):

type InteractiveElement = HTMLInputElement | HTMLSelectElement;

function getValue<T extends InteractiveElement = never>(): Array<T> | string {
  return null as unknown as Array<T> | string; // 
}

const inputValue = getValue<HTMLInputElement>(); // should be ChangeEvent<HTMLInputElement>
const selectValue = getValue<HTMLSelectElement>(); // should be ChangeEvent<HTMLSelectElement>
const emptyValue = getValue(); // should be string

Solution

  • You can do this either by overloading getValue() so the non-generic call signature is first:

    function getValue(): string;
    function getValue<T extends InteractiveElement>(): Array<T>;
    function getValue() {
        return null!
    }
    
    const inputValue = getValue<HTMLInputElement>(); // Array<HTMLInputElement>
    const selectValue = getValue<HTMLSelectElement>(); // Array<HTMLSelectElement>
    const emptyValue = getValue(); // string
    

    Or you can make the function return a conditional type which checks if T is never (which requires that the conditional type not be distributive, hence the wrapping with [+]) and returns string if so, or Array<T> if not:

    function getValue<T extends InteractiveElement = never>():
        [T] extends [never] ? string : Array<T> {
        return null!
    }
    
    const inputValue = getValue<HTMLInputElement>(); // Array<HTMLInputElement>
    const selectValue = getValue<HTMLSelectElement>(); // Array<HTMLSelectElement>
    const emptyValue = getValue(); // string
    

    Either way, this example cannot be properly implemented since there are no actual function parameters for the JavaScript to act upon. But since you said it was "minified" maybe it doesn't matter.

    Playground link to code