Reputation: 9295
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
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}
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}
Upvotes: 1
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
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