I am facing an issue with TypeScript's strictNullChecks
setting. I have a function handleAction
which expects an argument of type MyType
.
type MyType = {
prop: MyEnum;
// ... other properties
};
enum MyEnum {
Value1,
Value2,
// ... other values
}
async function handleAction(item: MyType) {
// Function logic here
}
Now, I have an object myRecord
which has a property nestedItem
that can be null
.
type MyRecord = {
nestedItem: MyType | null;
// ... other properties
};
const myRecord: MyRecord = {
nestedItem: null, // or some MyType object
// ... other properties
};
I want to check if nestedItem
exists and if its prop
is Value1
. I tried two approaches:
const isCertainCondition =
myRecord.nestedItem &&
myRecord.nestedItem.prop === MyEnum.Value1;
if (isCertainCondition) {
await handleAction(myRecord.nestedItem); // Error here
}
if (
myRecord.nestedItem &&
myRecord.nestedItem.prop === MyEnum.Value1
) {
await handleAction(myRecord.nestedItem); // No error here
}
The first approach gives me a TypeScript error:
Argument of type 'MyType | null' is not assignable to parameter of type 'MyType'.
Type 'null' is not assignable to type 'MyType'
The second approach works without any error.
Why is TypeScript behaving differently between these two approaches?
The reason why you get different results here is Narrowing | TypeScript Handbook
When you have this type of an example, you use "Truthiness narrowing" to exclude null
:
async function example2() {
// 2. this works
if (myRecord.nestedItem) {
myRecord.nestedItem
// ^? MyType
await handleAction(myRecord.nestedItem);
// ^? MyType
}
}
Same happens here but you keep the narrowed information in the variable nestedItem
:
async function example3() {
const nestedItem = myRecord.nestedItem;
// 3. this works
if (nestedItem) {
nestedItem
// ^? MyType
await handleAction(nestedItem);
// ^? MyType
}
}
When you keep the check in the variable nestedItem
and try to access myRecord.nestedItem
, you lose the narrowed information because it's only located in the variable nestedItem
:
async function example4() {
const nestedItem = myRecord.nestedItem;
// 4. this does NOT work now because nestedItem and myRecord.nestedItem disconnected from each other
if (nestedItem) {
nestedItem
// ^? MyType
await handleAction(myRecord.nestedItem);
// ^? MyType | null
}
}
Although you can mitigate this by creating type predicate, e.g. isDefined
:
const isDefined = Boolean as unknown as <Type>(value: Type) => value is NonNullable<Type>;
So instead of truthiness check you can use type predicate:
async function example5() {
// 5. nestedItem is narrowed down by type predicate
if (isDefined(myRecord.nestedItem)) {
await handleAction(myRecord.nestedItem);
// ^? MyType
}
}
Let me know if you have more questions
TypeScript playground with all examples step by step - https://tsplay.dev/m0V4rw