#What's the best way to check for an optional property in an object?

9 messages · Page 1 of 1 (latest)

verbal oyster
#

Should be simple, right? Just use the in operator (playground: http://tiny.cc/sgd9wz):

interface Foo { foo: string; }
interface Bar { bar: string; }

(foobar: Foo | Bar) => {
  // @ts-expect-error: Property 'foo' does not exist on type 'Foo | Bar'
  (typeof foobar['foo'] === 'string');

  // @ts-expect-error: narrowing with Object.hasOwn is not yet supported https://github.com/microsoft/TypeScript/issues/44253
  (Object.hasOwn(foobar, 'foo') && foobar['foo']);

  // good! this works and is suggested :)
  (('foo' in foobar) && foobar['foo']);
}

But there's a problem with the in operator - it doesn't work as you'd expect when you have objects with optional properties. This is valid and has no type errors:

interface Foo { foo?: string; }

const test = (foo: Foo) => {
  // oh dear... you probably didn't mean this :)
  (('foo' in foo) && foo['foo']);
}

// Both of these are valid but not equivalent
test({})
test({ foo: undefined })

This is obvious when you think about it and consider:

> "foo" in { "foo": undefined }
true

...which is why https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes is a thing.

With this flag enabled, you'd get a TS error on test({ foo: undefined }) to guard against this: (playground: http://tiny.cc/xsd9wz)

Ok cool, so is the answer "use the in operator and enable exactOptionalPropertyTypes"? I think it is today, but what about this (playground: http://tiny.cc/vvd9wz)...

interface Foo { foo?: string; }

const test = (foo: Foo) => {
  (('foo' in foo) && foo['foo']);
}

// @ts-expect-error: TS catches this (good)
test({ foo: undefined });

// @ts-expect-error: TS catches this (good)
test({ foo: undefined } as Foo);

// what about an indirection? uh oh...
const _foo: Foo = {};
test({ foo: _foo.foo } as Foo)

So this breaks. .foo here is undefined. I never explictly wrote : undefined so TS didn't catch it, but that's what it coerced into!

inland locust
#

assertions are unsafe. remove the assertion and you get an error as expected

#

btw, playgrounds are embedded here.

viral wadiBOT
#
that_guy977#0

Preview:ts ... const _foo: Foo = {} test({foo: _foo.foo} /* as Foo */)

inland locust
#

@verbal oyster

verbal oyster
#

@inland locust thanks for the note about playgrounds being embedded.

So yeah i was just trying to clarify the expected behaviour (errors), and show the case where it doesn't get caught at the end. I'm not sure that commenting out as Foo is a fix - sometimes that's required.

inland locust
#

assertions are unsafe, that's kinda a side effect of what typescript is

#

if it's required, then isn't it doing its job in supressing an error?

#

{ foo: _foo.foo } isn't a Foo, but it's close enough to one that an assertion is acceptable to ts