fix[omitBy, pickBy]: keep index signature types - #2019
Conversation
`omitBy` and `pickBy` always returned `Partial<T>`, so results from
objects typed with an index signature could not be assigned back to the
original type:
type ItemLookup = Record<number, Item>;
declare function processItems(items: ItemLookup): void;
processItems(omitBy(items, i => i.name.startsWith('A')));
// Argument of type 'Partial<ItemLookup>' is not assignable to
// parameter of type 'ItemLookup'
`Record<string, T>` and `Record<number, T>` already allow any key to be
missing, so wrapping them in `Partial` adds nothing but breaks
assignability. Return `T` unchanged when `T` has a string or number
index signature, and keep `Partial<T>` for objects with known keys,
where any property may genuinely have been dropped.
This is a type-only change; the implementations are untouched.
Closes toss#1987
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Small thing on the framing: You flagged the mixed case as a trade-off. I think it's avoidable rather than something to pick. Branching on the shape leaves three: Record<string, number> & { required: number } // required stays required, omitBy can still drop it
{ [k: symbol]: number } // still Partial, same bug as the issue
union input // falls back to Partial<T>Splitting T instead of branching on it gets all three: type PickIndexSignature<T> = { [K in keyof T as {} extends Record<K, unknown> ? K : never]: T[K] };
type OmitIndexSignature<T> = { [K in keyof T as {} extends Record<K, unknown> ? never : K]: T[K] };
export type OmitByResult<T> = Simplify<PickIndexSignature<T> & Partial<OmitIndexSignature<T>>>;
No cases in it, just the rule. A key that was already allowed to be missing stays as it is, a declared key gets marked optional. Tried it locally since it's type-only: full suite and typecheck pass. The one assertion that changes is the mixed-key one, which becomes |
Summary
omitByandpickByalways returnPartial<T>. For objects typed with an index signature that is wrong in a way that breaks real code — the result can no longer be assigned back to the type it came from:Record<string, T>andRecord<number, T>already allow any key to be absent, so wrapping them inPartialadds nothing and only breaks assignability. Objects with known keys are a different case:omitByreally can drop any of them, soPartial<T>stays correct there.es-toolkit/compatalready draws this line through overloads.fixes #1987
Changes
omitByandpickBynow returnTwhenThas a string or number index signature, andPartial<T>otherwise.Partial<T>.I used a conditional return type rather than the overloads suggested in the issue. With overloads, an object literal type such as
{ a: number; b: string }also matches theRecord<string, T>signature through its implicit index signature, which would change the return type for the common case.One case this does not cover, and it is a trade-off rather than an omission: for a type that mixes an index signature with declared properties, such as
interface Mixed { [key: string]: unknown; id: number }, the result keepsidas required even thoughomitBymay have dropped it. Today that same type returnsPartial<Mixed>, which is sound but cannot be assigned back. The index signature branch wins here, and the overloads suggested in the issue behave the same way. If you would rather keepPartial<T>for these, I can restrict the branch to types whose keys are only an index signature.