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!
From allowJs to useDefineForClassFields the TSConfig reference includes information about all of the active compiler flags setting up a TypeScript project.