In the realm of TypeScript, input in obj
currently serves as a type guard for obj
, but not for input
. There is a pending feature request at microsoft/TypeScript#43284 requesting it to function as a type guard for input
as well. However, implementation seems unlikely due to concerns mentioned in this comment, where it is deemed that such behavior could lead to issues with existing type guards.
It's important to note that this form of narrowing has always been viewed skeptically, as TypeScript types are not "sealed" (or "exact," as referred to in microsoft/TypeScript#12936). Just because input in obj
returns true, it does not confirm that input
is one of the known keys of typeof obj
. To illustrate, consider the following alteration to your code:
const _obj = {
foo: 'huh',
bar: 'hmm',
baz: 'oops'
}
const obj: { foo: string, bar: string } = _obj; // valid
Despite the change, the compilation proceeds without errors since the type {foo: string, bar: string}
allows for additional properties. If TypeScript underwent the desired narrowing, input in obj
would limit input
to "foo" | "bar"
, yet, it might still be "baz"
. Thus, such a method of narrowing would pose risks.
This doesn't mean it should never be considered; after all, TypeScript is not entirely foolproof when it comes to type safety. Applying a similar argument, it wouldn't be entirely secure to narrow a value o
of type {a: string} | {b: string}
based on "a" in o
, although it currently happens:
const a: { a: string } = { a: "" };
const _b = { b: "", a: 123 };
const b: { b: string } = _b;
const o = Math.random() < 0.5 ? a : b;
if ("a" in o) {
o // narrowed to { a: string }, unsafely
o.a.toUpperCase()
}
While soundness is a crucial aspect, there are other factors to consider. Until any potential shift in perspective from the TS team occurs, the desired narrowing may not materialize. Alternately, finding workarounds becomes imperative.
One simple workaround involves crafting a user-defined type guard function that mimics the anticipated behavior of input in obj
. In microsoft/TypeScript#43284, such a function is provided:
const in_ = <K extends string, O extends object>(key: K, object: O):
key is K & keyof O => key in object;
By substituting in_(input, obj)
for input in obj
, you can observe the expected outcomes:
function func(input: UnionType) {
if (in_(input, obj)) {
input
//^? (parameter) input: "foo" | "bar"
} else {
input
//^? (parameter) input: "baz"
}
}
The matter of soundness remains subject to your judgment based on the particular use case at hand.
Playground link to code