JHH
JHH

Reputation: 9295

Function that checks type guard against union type and returns a type which is the opposite of the detected type

Is it possible to create a function that checks if a value of union type A | B is A and if so throws an error, otherwise returns the value as B, so that the caller can disregard A?

Example:

type FailedResult = {
  error: string;
};

function isFailed(result: any): result is FailedResult {
  return 'error' in result;
}

function processResult<T>(result: T)  {
  if (isFailed(result)) {
    throw new Error('Error!');
  }

  return result; // If T was "A | FailedResult" we now want to return as A, i.e., "T minus FailedResult"
}

const result: {foo: number} | {error: string} = {foo: 42};

const notFailed = processResult(result); // want notFailed to be typed as {foo: number}

I tried using an infer type, but unsurprisingly it did not work:

type NotError<T> = T extends FailedResult | infer A ? A : never;

... supposedly because if T is of type A | FailedResult then A could still be {foo: number} | FailedResult.

So what I need is to be able to say that if this type A | {error :string} is not {error :string}, I want to infer a type which is not simply A (since A could itself contain {error: string}) but rather A | {error: string} _minus {error: string}. In other words, if T is A | B and I know it's not A I want to get the type T minus A.

Upvotes: 0

Views: 41

Answers (3)

Oblosys
Oblosys

Reputation: 15106

If you make sure the argument is always a union type, you can just add | FailedResult to the result parameter:

function processResult<T>(result: T | FailedResult)  {
  if (isFailed(result)) {
    throw new Error('Error!');
  }

  return result;
}

const result: {foo: number} | {error: string} = {foo: 42};

const notFailed = processResult(result); // inferred type: {foo: number}

If you call processResult with a bare FailedResult it will infer a FailedResult return type though, whereas never would be more appropriate.

const noUnion = processResult({error: 'oops'}) // inferred type: {error: string}

TypeScript playground

Alternatively, you can use an explicit cast on the result of processResult, making the type guard redundant. For this you could define a type

type ExcludeFailedResult<T> =
  T extends FailedResult ? FailedResult extends T ? never : T : T 

As ExcludeFailedResult distributes over unions, ExcludeFailedResult<T1 | FailedResult> evaluates to ExcludeFailedResult<T1> | ExcludeFailedResult<FailedResult>, yielding T1. The double extends conditions make sure only the exact FailedResult gets filtered out.

Using ExcludeFailedResult, processResult becomes

function processResult<T>(result: T)  {
  if (isFailed(result)) {
    throw new Error('Error!');
  }

  return result as ExcludeFailedResult<T>;
}

const notFailed = processResult({} as {foo: number} | {error: string});
// inferred type: {foo: number}

const noUnion = processResult({error: 'oops'})
// inferred type: never

const overlap = processResult({} as {foo: number, error: string} | {error: string})
// inferred type: {foo: number} | {error: string}

TypeScript playground

Upvotes: 1

JHH
JHH

Reputation: 9295

After playing around some more I realized it's probably as easy as T extends FailedResult ? never : T, i.e., something like this:

type FailedResult = {
  error: string;
};

type NotError<T> = T extends FailedResult ? never : T;

function isFailed(result: any): result is FailedResult {
  return 'error' in result;
}

function processResult<T>(result: T): NotError<T>  {
  if (isFailed(result)) {
    throw new Error('Error!');
  }
  return result as NotError<T>;
}

function test(r: {foo: number} | FailedResult) {
  const notError = processResult(r)
  
  const foo: number = notError.foo; // we now that notError is of type {foo: number}
}

Upvotes: 0

DedaDev
DedaDev

Reputation: 5249

Looks nasty but you can do this:

function processResult<T extends typeof result>(result: T)  {
  if (isFailed(result as FailedResult)) {
    throw new Error('Error!');
  }
  return result;
}

Upvotes: 0

Related Questions