Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to include undefined in a return type only if an argument could be undefined using Exclude<>?

Tags:

typescript

I'm trying to make a function that takes string and returns number include undefined in the return type only if the argument included it.

Here's what I have (which I thought would work):

export function test<T extends string | undefined>(a: T)
  :Exclude<boolean | T, string> {
    if (a === undefined)
        return undefined;
    return true;
 }

I hoped that Exclude<boolean | T, string> would remove the string from the string | undefined leaving either undefined or nothing (depending on the supplied argument type) however this code does not type-check, it says:

Type 'undefined' is not assignable to type 'boolean | Exclude<T, string>'.

like image 510
Danny Tuppeny Avatar asked Dec 18 '22 18:12

Danny Tuppeny


2 Answers

Unresolved conditional types (like Exclude<boolean | T, string> with T generic) are not often assignable. The compiler doesn't really know how to determine if something is of such a type because it does not try to iterate through all possible instantiations of the generic T to see if the assignment is safe. So in these cases you usually assert that a value is that type, or you use function overloads so that the function implementation uses plain-old union types instead of conditional types. Here's the assertion solution:

export function test<T extends string | undefined>(a: T): Exclude<boolean | T, string> {
  if (a === undefined)
    return undefined as Exclude<boolean | T, string>; // asserted
  return true;
}

And here's the overload solution:

export function test<T extends string | undefined>(a: T): Exclude<boolean | T, string>;
// overloaded
export function test(a: string | undefined): boolean | undefined {
  if (a === undefined)
    return undefined;
  return true;
}

As long as they work the way you want when you call them with concretely-typed values, then conditional types should behave how you expect:

const definitelyDefined = test("hey"); // boolean, okay
const maybeUndefined = test(Math.random()<0.5 ? "hey" : undefined); // boolean | undefined, okay

(As an aside, I'd probably render your return type as boolean | (undefined extends T ? undefined : never):

export function test<T extends string | undefined>(
  a: T
): boolean | (undefined extends T ? undefined : never) {
  if (a === undefined)
    return undefined as (undefined extends T ? undefined : never); // asserted
  return true;
}

But that's just a matter of preference.)

Okay, hope that helps; good luck!

like image 144
jcalz Avatar answered May 23 '23 08:05

jcalz


You could use function overloads to achieve this. Start by making your function accept either type and return either type, then add overloaded definitions to restrict which parameter & return types can be used together.

function test(a: string): boolean;
function test(a: undefined): undefined;

function test(a: string | undefined): boolean | undefined {
  if (a === undefined)
    return undefined;

  return true;
}
like image 24
MTCoster Avatar answered May 23 '23 07:05

MTCoster