5

I'm trying to type guard an unknown type

const foo  = (obj: unknown) => {
    if (typeof obj === 'object' && obj) {
        if ('foo' in obj && typeof obj.foo === 'string') {
            return obj.foo;
        }
    }
};

But I'm getting

Property 'foo' does not exist on type 'object'.

I also tried with is expression does not work:

const foo  = (obj: unknown): obj is { foo: 'string' } => {
    if (typeof obj === 'object' && obj) {
        if ('foo' in obj && typeof obj.foo === 'string') {
            return obj;
        }
    }
    throw new Error();
};
Vinz243
  • 9,654
  • 10
  • 42
  • 86
  • try to cast `obj` to `any` before checking for `foo` – Ricky Mo Oct 25 '21 at 09:19
  • @RickyMo you mean `(obj: any) => {...` ? – Vinz243 Oct 25 '21 at 09:19
  • 1
    Is there a specific reason you're using `unknown`? `any` would work, but you've specifically mentioned `unknown` so... – T.J. Crowder Oct 25 '21 at 09:21
  • [https://stackoverflow.com/questions/51439843/unknown-vs-any](https://stackoverflow.com/questions/51439843/unknown-vs-any) – Ricky Mo Oct 25 '21 at 09:22
  • I'm using gts preset, which throws me a warning when using any. And I prefer to type check the code because it's something written by the user – Vinz243 Oct 25 '21 at 09:24
  • I tried with the cast @RickyMo it still shows an error https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZiEMYF4YAoQCMBWAuGAVzAGswQB3MASlQD4YBvAKGWQEs4MoBPABwCmILthyoUaAOSiBwKJJgAyRTFG0WbTTFCRYWAIYAnVKtwx9EUzllQA3Ky0cu6SQhAL2YGAePKYvQWFvIwA6N3EpaENPAHNJdQdHTUMBKEJDLx8wxHskmABfRILmfPtmIA – Vinz243 Oct 25 '21 at 09:24
  • any works but it's not what i want – Vinz243 Oct 25 '21 at 09:25
  • So what do you want? – Ricky Mo Oct 25 '21 at 09:25
  • By `const bar = obj as any`, you can access properties of `obj`. You need it to be `any` to access its properties. You then checked `'foo' in bar && typeof bar.foo === 'string'`, which fullfill your requirement. What else do you want? – Ricky Mo Oct 25 '21 at 09:28
  • In this case `unknown` doesn't add any additional type safety. You could just: `const foo = (obj: any) => obj?.foo === 'string' ? obj?.foo : undefined` – Aleksey L. Oct 25 '21 at 10:48

2 Answers2

3

You're going to have to give TypeScript a little help here:

type fooObj = object & { foo: unknown };
const foo = (obj: unknown) => {
    if (typeof obj === 'object' && obj) {
        if ('foo' in obj && typeof (obj as fooObj).foo === 'string') {
            return (obj as fooObj).foo;
        }
    }
};
Cerbrus
  • 70,800
  • 18
  • 132
  • 147
1

Please consider using this helper:

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
  : obj is Obj & Record<Prop, unknown> =>
  Object.prototype.hasOwnProperty.call(obj, prop);

in your case. in operator works as expected mostly with unions. Please see here, here and here

Working solution:

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
  : obj is Obj & Record<Prop, unknown> =>
  Object.prototype.hasOwnProperty.call(obj, prop);

const foo = (obj: unknown) => {
  if (typeof obj === 'object' && obj) {
    if (hasProperty(obj, 'foo') && typeof obj.foo === 'string') {
      return obj.foo;
    }
  }
};

Playground

However, since you want to throw an error if obj is invalid, you can use assert function:

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
  : obj is Obj & Record<Prop, unknown> =>
  Object.prototype.hasOwnProperty.call(obj, prop);

function foo(obj: unknown): asserts obj is { foo: string } {
  const isValid =
    typeof obj === 'object' &&
    obj &&
    hasProperty(obj, 'foo') &&
    typeof obj.foo === 'string';

  if (!isValid) {
    throw new Error();
  }

};

declare var obj: unknown;

foo(obj);

obj.foo // ok

Playground