Consider a union of two types with a type predicate
type Maybe<T> = T | Error
function isValid<T>(value: Maybe<T>): value is T {
return !(value instanceof Error)
}
Lets have code with several computed Maybe
values and we want a type predicate which summarily checks all of them to not be an Error
:
const a: Maybe<string> = ... // some function returning Maybe<string>
const b: Maybe<number> = ...
...
if (![a, b, /*and more*/].every(isValid)) {
return;
}
const realA: string = a;
//Type 'Maybe<string>' is not assignable to type 'string'.
// Type 'Error' is not assignable to type 'string'.(2322)
Despite the every(isValid)
the compiler cannot derive that none of a
, b
,... is a Maybe
any more.
Of course I could write
if (!isValid(a) && !isValid(b) && ...) {
return;
}
to help the compiler along. But I have 5 to 10 a
s and b
s and am in general curious if there is a solution. If have tried
const data = [a, b].filter(isValid)
but this creates a mishmash array type like (string | number)[]
, so that a data[0]
is still not decisively a string
.
So neither every
nor filter
do the trick of allowing the compiler to strip the Maybe
afterwards, but I wonder if there is a summarily, "loopy" way which avoids a cumbersome if(!isValid(a)...
sequence.
There is currently no way to use a custom type guard function to narrow just the contents or members of a value.
Custom type guard functions, which return a type predicate
of the form arg is Type
or this is Type
, only narrow the apparent type of the specific argument corresponding to arg
or the object corresponding to this
. Any effects of narrowing would only be seen through subsequent accesses of the identical value. Custom type guard functions do not separately affect the apparent types of values which were assigned to members of arg
or this
. For example:
declare function g(
x: { a: RegExp | Date }
): x is { a: Date };
const a = Math.random()<0.5 ? new Date() : new RegExp("")
// ^? const a: RegExp | Date
const x = { a };
// ^? const x: { a:RegExp | Date; }
if (g(x)) {
x.a;
//^? (property) a: Date
a;
//^? const a: RegExp | Date
}
In the above, calling g(x)
has the effect of narrowing the apparent type of x
to {a: Date}
, and therefore narrowing the apparent type of x.a
to Date
. But a
, a reference to the same value as x.a
, has not been narrowed.
That means it is essentially useless to use a custom type guard function on an anonymous object literal or an array literal:
if (g({ a })) {
a;
//^? const a: RegExp | Date
({ a }).a
// ^? (property) a: RegExp | Date
}
The object literal { a }
cannot be accessed again; it's anonymous. Writing { a }
a second time just gives us a new object. And we know that a
itself doesn't get narrowed. So we're stuck.
There's an open feature request at microsoft/TypeScript#46184 asking for support to narrow object contents (specifically the destructured variables of object literals; presumably for array literals as well), but for now it's not part of the language.
--
You were trying to narrow an array literal [a, b, ⋯]
with the type guard call signature for the every()
method, but as we have just seen, this wouldn't have any effect:
const now = new Date().getTime();
const a: Maybe<string> = now > 1 ? 'a' : new Error;
const b: Maybe<number> = now > 1 ? 1 : new Error;
const c: Maybe<null> = now > 1 ? null : new Error;
if ([a, b, c].every(isValid)) {
a.toUpperCase(); // error, Maybe<string> 😢
}
Okay, well, what if we assign the array literal to a variable first and access the members through indices later? That fixes the problem with being anonymous:
const arr = [a, b, c];
// const arr: (string | number | Error | null)[]
if (arr.every(isValid)) {
const newA = arr[0]; // const newA: string | number | null
}
So, we get narrowing, hooray. Kind of. Unfortunately the array arr
is of type Maybe<string | number | null>[]
. So even though we've narrowed arr[0]
to string | number | null
, we've completely lost track of the order of the inputs. That brings us to our next problem:
To keep track of the order of arr
's elements, we'd need it to have a tuple type instead. And every
doesn't act as an element-wise type guard on tuples:
const tup = [a, b, c] as const;
// const tup: readonly [Maybe<string>, Maybe<number>, Maybe<null>]
if (tup.every(isValid)) {
tup // const tup: readonly [Maybe<string>, Maybe<number>, Maybe<null>] 😟
}
And there's no way to fix that. TypeScript currently lacks the expressiveness necessary to describe how to perform element-wise type guards on tuples. It would require some way of talking about an arbitrary generic type F
at the type level, maybe like:
// NOT VALID TS, DO NOT TRY THIS
function every<T extends any[], F extends 🤷♂️>(
arr: T, cb: <U extends T[number]>(v: U, i: number, a: T) => value is F<U>
): {[I in keyof T]: F<T[I]>}
But you can't do that. F
is a generic type parameter that behaves like a specific type; you can't instantiate it with its own type arguments like F<U>
or F<T[I]>
. There is a longstanding open issue at microsoft/TypeScript#1213 but it's not part of the language. There are various ways to try to simulate/encode such types, but they are not user-friendly enough to suggest here.
See Mapping tuple-typed value to different tuple-typed value without casts for more details about a similar issue.
So we'll have to give up on using some kind of general type guard mapper that takes isValid
as an input, and just go ahead and hardcode isValid()
in there.
If we want to use an array, we can write
function isEveryElementValid<T extends readonly any[]>(
values: { [K in keyof T]: Maybe<T[K]> }): values is T {
return values.every(isValid);
}
and then use it like
if (isEveryElementValid(tup)) {
const realA = tup[0]; // const realA: string
const realB = tup[1]; // const realB: number
const realC = tup[2]; // const realC: null
}
Or if we want to use an object, we can write
function isEveryPropertyValid<T extends object>(
values: { [K in keyof T]: Maybe<T[K]> }): values is T {
return Object.values(values).every(isValid);
}
and then use it like
const o = {a,b,c};
if (isEveryPropertyValid(o)) {
const realA = o.a; // string
const realB = o.b; // number
const realC = o.c; // null
}