Reputation: 385764
New to TypeScript. I have something like this:
class Foo {
dowork(data: object): void {
if ('some_prop' in data && Array.isArray(data.some_props)) {
// ...
}
}
main(): void {
const json = '{ ... }';
const data = JSON.parse(json);
if (data instanceof Object) {
this.dowork(data);
}
}
( new Foo() ).main();
It fails with
src/index.ts:388:53 - error TS2339: Property 'some_prop' does not exist on type 'object'.
388 if ('some_prop' in data && Array.isArray(data.some_prop)) {
~~~~~~~~~
I believe I can get around the issue using
interface ObjectWithSomeProp {
some_prop: any,
}
function is_object_with_some_prop(x: any): x is ObjectWithSomeProp {
return typeof x === 'object' && 'some_prop' in x;
}
I will be doing multiple such tests. Do I need to create an interface and test for each one, or is there a cleaner way of doing this?
Upvotes: 2
Views: 1557
Reputation: 704
Make a generic hasProp
typeguard
const hasProp = <T extends string>(obj: object, prop: T): obj is { [K in T]: any } => {
return prop in obj;
}
const someFunc = (obj: object) => {
// obj is of type object and we have no clue if he has 'foo' prop or not
if (hasProp(obj, 'foo')) {
// obj is of type { foo: any }, so we can use foo now
const some = obj.foo;
}
}
you will have what you need, but it will break type of obj, it is not a solution if you want to use some props of obj
other than foo
in the if
block.
Add second generic to describe object type and keep it as is
const hasProp = <O extends object, T extends string>(obj: O, prop: T): obj is O & { [K in T]: any } => {
return prop in obj;
}
const someFunc = (obj: { bar: number }) => {
// obj is of type { bar: number } and 'foo' prop is not here
if (hasProp(obj, 'foo')) {
// obj is of type { bar: number } & { foo: any }
// so we managed to persist previous obj type and add 'foo' after typeguard use
const fooProp = obj.foo;
const barProp = obj.bar;
}
}
We did not break obj type and managed to add foo prop, but still, foo type is any
. And probably we want to persist type of foo prop too, so we need to infer its type from obj.
We first define a type for ObjectWithProp
, what keeps a type of the object, if it already has foo
prop, and adds it as any
if not
type ObjectWithProp<O extends object, P extends string> = P extends keyof O
? O
: O & { [K in P]: any };
Then we just use it in the typeguard and have desired result
const hasProp = <O extends object, P extends string>(obj: O, prop: P): obj is ObjectWithProp<O, P> => {
return prop in obj;
}
const someFunc = (obj: { bar: number, foo: string }) => {
if (hasProp(obj, 'foo')) {
// obj kept it's type and we can still see that foo is a string
const some = obj.foo;
}
}
const someFunc2 = (obj: { bar: number }) => {
if (hasProp(obj, 'foo')) {
// if obj has no 'foo' prop initially, then it will be added as `any`
const some = obj.foo;
}
}
Upvotes: 2
Reputation: 385764
I defined these types and helpers:
type JsonValue = null | string | number | boolean | JsonArray | JsonDict;
type JsonArray = JsonValue[];
interface JsonDict extends Record<string, JsonValue> { }
function is_json_dict(x: JsonValue): x is JsonDict {
return x instanceof Object && !Array.isArray(x);
}
// For symmetry. Could simply use Array.isArray(x).
function is_json_array(x: JsonValue): x is JsonArray {
return Array.isArray(x);
}
One can use the near-identical code:
class Foo {
dowork(data: JsonDict): void {
if (is_json_array(data.some_props)) {
// ...
}
}
main(): void {
const json = '{ ... }';
const data: JsonValue = JSON.parse(json);
if (is_json_dict(data)) {
this.dowork(data);
}
}
( new Foo() ).main();
Upvotes: 0