Unfortunately, the compiler cannot follow your logic at the type-level strongly enough to give you the types you're looking for. At each step along the way: filter(), map(), and Object.fromEntries(), the compiler produces a correct but too-wide-to-be-useful type.
Ultimately you're going to want to use a type assertion like this:
return Object.fromEntries(
parts
.filter(({ type }) => ["year", "month", "day"].includes(type))
.map(({ type, value }) => [type, Number(value)])
) as Record<'year' | 'month' | 'day', number>;
This is a reasonable approach for situations, like this one, where you know more than the compiler about the type of a value.
The alternative is to provide your own more specific typings for/to filter(), map(), and Object.fromEntries(), which are likely to be useful only for this specific line of code, and have their own caveats.
For example, the compiler does not realize that ({type} => ["year", "month", "day"].includes(type)) acts as a type guard on its input. Type guard functions are not inferred automatically; you would have to annotate it yourself as a user-defined type guard function, like this:
const filteredParts = parts
.filter((part): part is { type: "year" | "month" | "day", value: string } =>
["year", "month", "day"].includes(part.type))
/* const filteredParts: {
type: "year" | "month" | "day";
value: string;
}[] */
Now the output of filter() will be narrow enough. The caveat here is that the compiler just believes you that a user-defined type guard does what it says it's doing, so it's like a type assertion. Had you written ["foo", "bar", "baz"].includes(part.type), instead, the compiler wouldn't notice or warn you.
Then, when you return an array literal inside map() like [key, val], the compiler will widen it to an unordered array type like Array<typeof key | typeof val>, whereas you specifically need a tuple type like [typeof key, typeof val]. So you'll need to change that behavior, possibly by using a const assertion:
const mappedParts = filteredParts.map(
({ type, value }) => [type, Number(value)] as const
);
// const mappedParts: (readonly ["year" | "month" | "day", number])[]
Great, now you have something to pass to Object.fromEntries(). Unfortunately, the TypeScript standard library's type definitions for Object.fromEntries() just returns either any, or something with a string index signature.
If you want, you could merge in your own type signature that returns a key-remapped type which more accurately represents things (and it will have to be a global augmentation if you are using modules), like this:
interface ObjectConstructor {
fromEntries<T extends readonly [PropertyKey, any]>(
entries: Iterable<T>
): { [K in T as T[0]]: T[1] };
}
And then, finally, your return value will be strongly typed the way you want:
const ret = Object.fromEntries(mappedParts);
/* const ret: {
year: number;
month: number;
day: number;
} */
Is it worth all that? I doubt it. It's up to you though.
Playground link to code