- Since TypeScript 3.0 introduced the
unknown
top type in mid-2018, use of theany
type is discouraged. - TypeScript also had had long support for succint type-guards with the
typeof
operator, buttypeof
only works as a first-class type-guard for scalar values (i.e. single variables).- The major caveat being that it cannot be used with object properties or array elements without first using
as any
oras T
.- Using
as any
has immediately obvious problems. - But using
as T
also introduces its own problems. This isn't that big a problem inside a type-guard function as the scope of the variable-with-assumed-type is limited to the type-guard, but if used inside a normal function it can introduce bugs.
- Using
- The major caveat being that it cannot be used with object properties or array elements without first using
I'm currently writing client-side error-handling code in TypeScript, in particular, I'm writing an event-listener for window.error
, which receives an ErrorEvent
object which in-turn has a member property named error
which in practice could be anything depending on various circumstances.
In TypeScript we need to write top-level functions that serve as runtime and compile-time type-guards. For example, to check if the window.error
event-listener is really receiving an ErrorEvent
instead of an Event
I'd write this:
function isErrorEvent( e: unknown ): e is ErrorEvent {
// TODO
}
function onWindowError( e: unknown ): void {
if( isErrorEvent( e ) ) {
// do stuff with `e.error`, etc.
}
}
window.addEventListener( 'error', onWindowError );
My question is about how I'm meant to idiomatically implement isErrorEvent
the way that the TypeScript language designers intend-for. I haven't been able to find any authoritative documentation on the subject.
Specifically, I don't know how I'm supposed to use runtime typeof
checks to implement isErrorEvent
without using either a type-assertion to any
or to the destination type ErrorEvent
. As far as I know, either of those techniques is required because TypeScript will not let you use typeof x.y
when y
is not part of x
's static type - which strikes me as odd because TypeScript does let you use typeof x
when x
is a scalar of any type, not just its static type.
Below, using as any
works, but I don't like the lack of safety from the asAny.colno
property dereferences:
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
const asAny = e as any;
return (
typeof asAny.colno === 'number' &&
typeof asAny.error === 'object' &&
typeof asAny.lineno === 'number'
);
}
The alternative is to use as ErrorEvent
, but I feel that's just as unsafe because TypeScript then allows dereferencing of members of e
without a prior typeof
check!
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
const assumed = e as ErrorEvent;
return (
typeof assumed.colno === 'number' &&
typeof assumed.error === 'object' &&
typeof assumed.lineno === 'number' &&
// For example, TypeScript will not complain about the line below, even though I haven't proved that `e.message` actually exists, just because `ErrorEvent.message` is defined in `lib.dom.d.ts`:
assumed.message.length > 0
);
}
I suppose what I'm asking is how can I make something like this (see below) work, where TypeScript requires every member be checked with typeof
before allowing any dereference, and allowing e
to retain its static-type as unknown
:
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
return (
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
typeof e.message === 'string' &&
e.message.length > 0
);
}
...but TypeScript does let us do this (see below) which is arguably the same thing, just syntactically far more verbose:
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
const assume = e as ErrorEvent;
if(
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
)
{
const message = assume.message as any;
return typeof message === 'string' && message.length > 0;
}
}
any
isn't that bad. A type guard often receives something and it has to determine whether it fulfills the criteria to be something else. You often cannot do that statically, otherwise you don't really need the type guard. There are some instances where you can do this like a union ofA | B
and want to type guard forA
but for arbitrary values it's inherently unsafe. You can type asserte as Record<string, any>
if it makes you feel safer but I'm not sure there is a convenient way to be completely type safe here. – Unspeakabletypeof x.y
(wherex
isunknown
) isn’t allowed by TypeScript - so I was wondering if I was overlooking something simple. I suppose I could check fortypeof object
to allow the arbitrary string property indexer to be used though... – Bergeron