Hantera godtyckliga objekt i TypeScript


Ny på TypeScript. Jag har något som detta:

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();

Det misslyckas med

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)) {
                                                        ~~~~~~~~~

Jag tror att jag kan kringgå problemet genom att använda

interface ObjectWithSomeProp {
   some_prop: any,
}

function is_object_with_some_prop(x: any): x is ObjectWithSomeProp {
   return typeof x === 'object' && 'some_prop' in x;
}

Jag kommer att göra flera sådana tester. Behöver jag skapa ett gränssnitt och testa för varje, eller finns det ett renare sätt att göra detta på?

Svar #1

Enklaste sättet

Skapa en generell hasProp-typvakt

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

du kommer att ha det du behöver, men det kommer att bryta typen av obj, det är inte en lösning om du vill använda några egenskaper av obj annat än foo i if-blocket.

Lite bättre

Lägg till en andra generisk för att beskriva objekttypen och behåll den som den är

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

Vi bröt inte obj-typen och lyckades lägga till foo-egenskapen, men ändå är foo-typen any. Och troligen vill vi behålla typen för foo-egenskapen också, så vi behöver härleda dess typ från obj.

Bästa lösning

Vi definierar först en typ för ObjectWithProp, som behåller typen av objektet om det redan har foo-egenskapen, och lägger till den som any om det inte har det

type ObjectWithProp<O extends object, P extends string> = P extends keyof O
  ? O
  : O & { [K in P]: any };

Då använder vi det bara i typeguarden och får önskat resultat

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

Lekplatslänk

Svar #2

Jag definierade dessa typer och hjälpmetoder:

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

Man kan använda den nästan identiska koden:

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();