Can someone explain why inferred type predicates don’t work on an array of objects if the type is explicitly set?
According to this announcement, from version 5.5 on, TypeScript is able to infer the new type when an array is filtered.
For example:
const arr0 = [ 1, 2, 3, undefined, 4, 5, undefined];
// ^? const arr0: (number | undefined)[]
const numsOnly0 = arr0.filter(n => typeof n === "number");
// ^? const numsOnly0: number[]
In this simple case, it also works if the array is explicitly typed:
type NumOrUndefined = number | undefined;
const arr1:NumOrUndefined[] = [ 1, 2, 3, undefined, 4, 5, undefined];
// ^? const arr1: NumOrUndefined[]
const numsOnly1 = arr1.filter(n => typeof n === "number");
// ^? const numsOnly0: number[]
TypeScript knows, that after filtering, the previous type NumOrUndefined[]
isn’t narrow enough and infers number[]
.
I was excited to see this new feature to work in an actual codebase. What I found is that this inferring doesn’t seem to work for objects which are explicitly typed.
For example:
type OriginalType = { id: number | undefined; name: string | undefined }
const objArr1: OriginalType[] = [
// ^? const objArr1: OriginalType[]
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
]
// type is not inferred
const definedOnly1 = objArr1.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? const definedOnly1: OriginalType[]
Without the explicit typing, it does work:
const objArr0= [
// ^? objArr0: (({id: number; name: string;} | {id: undefined; name: string;} | {id: number; name: undefined;} | {id: undefined; name: undefined;})[]
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
]
// new type is inferred properly
const definedOnly0 = objArr0.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? definedOnly0: {id: number; name: string;}[]
One way of “helping” the TypeScript compiler seems to be using satisfies
instead of explicit typing. Although I find it not very convenient since this does not work for e.g. typed function parameters. But just for the sake of completness, here’s the example:
const objArr2 = [
// ^? same as objArr0
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
] satisfies OriginalType[];
// original type is used on original object, but also inferred on filtered object properly
const definedOnly2 = objArr2.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? same as definedOnly0
Can anyone explain if this is the intended behavior and why?
Playground
1