My application receives "messages". I first validate an unknown input to ensure it follows the expected message format:
const isMessage = x =>
typeof x === 'object' &&
x !== null &&
typeof x['data'] === 'string';
I wish to type this in TypeScript. Here's what I have:
type Message = { data: string };
const isMessage = (x: unknown): x is Message =>
typeof x === 'object' &&
x !== null &&
typeof x['data'] === 'string';
However, this fails to type-check, because:
Element implicitly has an 'any' type because expression of type '"data"' can't be used to index type '{}'.
Property 'data' does not exist on type '{}'.
After the type guard typeof x === 'object' && x !== null
, TypeScript gives the typing x : object
. This seems to be the same as x : {}
. But this type does not allow me to check any properties on the object.
Instead of x: object
, I think I want a "dictionary" type like x: { [key: string | number | symbol]: unknown }
. But this is not the typing that TypeScript gives me from the type guard typeof x === 'object'
.
I can use as
to cast x
to a dictionary type:
const isMessage = (x: unknown): x is Message =>
typeof x === 'object' &&
x !== null &&
typeof (x as { [key: string | number | symbol]: unknown })['data'] === 'string';
This type-checks, but it's really long and unwieldy, and I'm not sure the as
typecast is really type-safe.
I read about the in
operator narrowing, and based on this, I expected that adding 'data' in x
would work:
const isMessage = (x: unknown): x is Message =>
typeof x === 'object' &&
x !== null &&
'data' in x &&
typeof x['data'] === 'string';
However, this makes no difference; TypeScript still complains that I can't index into x
, even at a point where 'data' in x
. Why does this in
operator not allow me to index into x
?
CodePudding user response:
You should be able to do it like this:
type Message = { data: string };
const isMessage = (x: unknown): x is Message =>
typeof x === 'object' &&
x !== null &&
typeof (x as Message).data === 'string';
This technique is shown in TypeScript's docs: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Since typeof
is also a runtime check, the as
assertion does not remove any type safetyness.
You can think of it this way: Before the last line, we have already checked that x
is an object and is not null
. So, x.data
cannot fail at runtime, even if x
would be {}
or {bar: 'bar'}
or {data: null}
. We just need to use the assertion to make the compiler allow us to do the runtime typeof
check.
CodePudding user response:
You can use generic hasProperty
helper for property check:
type Message = { data: string };
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const isMessage = (x: unknown): x is Message =>
typeof x === 'object' &&
x !== null &&
hasProperty(x, 'data') &&
typeof x['data'] === 'string'
Please see my answer here for more context about in
operator and this issue/43284