2

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?

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

5 Comments

Re "Make a generic hasProp typeguard", First thing I tried after composing the question, but I failed. Thanks, I'll look at the other solutions you provided shortly. I'm sure I'll learn a lot :)
It was a good idea actually :). Also typescript default naming convention is using camel case for functions and variables, so probably you should not use snake case in JS.
There's a good reason CamelCase was universally avoided for over 30 years: It's a lot harder to read. It's especially hard to read for some non-native English speakers, since they would have a hard time distinguising where each word ends. There in lies the reason we put space between words. Spacing is good.
Also, it's very useful to use different conventions for type identifiers and other identifiers. As such, I follow the long-standing convention of using CamelCase for type identifers and not for others. If I collaborate on another project, I'll follow their conventions, but I'll stick to the CS ideal of maximizing readability and maintainability on my own projects.
I personally follow typescript guidelines and name variables and functions in camelCase, types in PascalCase and constants is ALL_CAPS. But it is totally up to you as long as it's your personal project, I only mentioned default convention.
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.