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