The compiler cannot automatically deduce from your implementation that exampleObj
does not have any properties with the value of undefined
.
Although you, as a human, can understand that applying type guarding to the array resulting from Object.values(obj)
impacts the type of obj
, TypeScript lacks the capability to represent this relationship explicitly. In general, when you perform type guarding on a value in TypeScript, it only affects the apparent type of that particular value itself (or, in cases where you check the discriminant property of an object belonging to a discriminated union, it can influence the apparent type of that object). Trying to extend type guarding effects through other operations would significantly harm compiler performance if implemented. As mentioned in a comment on microsoft/TypeScript#12185, which is a similar feature request,
This would necessitate tracking the implications/effects of a specific value for one variable onto other variables, adding complexity (and associated performance costs) to the control flow analyzer.
Therefore, if the compiler is unable to infer this by itself, we need to provide explicit instructions.
If you plan to use the Object.values(obj).every(...)
test only once in your codebase, then the most viable approach is to employ a type assertion:
const returnsExampleObjAssert = (): ExampleObj | undefined => {
let exampleObj = {
foo: getFoo(),
bar: getBar(),
baz: getBaz(),
}
return Object.values(exampleObj).every(val => val != undefined) ?
exampleObj as ExampleObj : undefined;
};
By using exampleObj as ExampleObj
, we are essentially asking the compiler to treat exampleObj
as if it were of type ExampleObj
. The compiler simply complies since it cannot definitively determine the truth either way. Hence, caution is advised against misleading the compiler (e.g., `Object.values(exampleObj).some(val => val != undefined) ? exampleObj as ExampleObj : undefined).
If you anticipate running this test multiple times on various objects, creating a user-defined type guard function with a return type of a type predicate in the form arg is Type
might be more beneficial. When calling such a function, the compiler recognizes that a true
outcome implies that arg
can be narrowed down to Type</code, while a <code>false
result indicates no such narrowing can occur (sometimes even implying another type of narrowing that excludes Type
). Here's how you could implement it for your testing scenario:
function allPropsDefined<T extends object>(
obj: T
): obj is { [K in keyof T]: Exclude<T[K], undefined> } {
return Object.values(obj).every(v => typeof v !== "undefined");
}
The allPropsDefined()
function takes an argument called obj
of a generic object-like type T
. Its implementation returns a boolean value: true
if all properties of obj
are defined and false
otherwise. The return type,
obj is { [K in keyof T]: Exclude<T[K], undefined> }
, serves as a type predicate that is assignable to
boolean
. This type
{ [K in keyof T]: Exclude<T[K], undefined> }
represents a
mapped type, containing the same keys as
T
, but with modified properties where
undefined
has been excluded via
the Exclude
utility type. For instance, if
T[K]
is
string | number | undefined
, then
Exclude<T[K], undefined>
becomes
string | number
.
Let's put it to the test:
const returnsExampleObjTypePredFunc = (): ExampleObj | undefined => {
let exampleObj = {
foo: getFoo(),
bar: getBar(),
baz: getBaz(),
}
return allPropsDefined(exampleObj) ?
exampleObj // let exampleObj: { foo: Foo; bar: Bar; baz: Baz; }
: undefined;
};
Now, this compilation proceeds without errors. You can observe that within the true clause of the ternary conditional operator, exampleObj
has been refined from
{foo: Foo | undefined, bar: Bar | undefined, baz: Baz | undefined}
to
{foo: Foo, bar: Bar, baz: Baz}
, which aligns with
ExampleObj
as intended.
Once more, if you're performing this test just once or twice, introducing a type predicate function might not be worthwhile. However, if you expect frequent usage across your codebase, implementing a type predicate function could offer benefits in efficiency.
Moreover, the compiler lacks the ability to confirm the correctness of your type guard function implementation. Changing from every()
to some()
would still appease the compiler. Ultimately, the compiler solely ensures that the return type matches boolean
. Therefore, exercising care not to deceive the compiler remains paramount.
Playground link to code