ikegami
ikegami

Reputation: 385764

Dealing with arbitrary objects in TypeScript

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

Answers (2)

Temoncher
Temoncher

Reputation: 704

Easiest way

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.

A little bit better

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.

Best solution

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;
  }
}

Playground link

Upvotes: 2

ikegami
ikegami

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

Related Questions