NonEmptyObject: Handle index signatures conservatively#1419
Conversation
Addresses sindresorhus#821. A type whose only members are index signatures cannot statically express "at least one dynamic key" in TypeScript, so `NonEmptyObject` previously degenerated to the input type and silently accepted `{}`. Re-derive the key set via `OmitIndexSignature` so: - Pure-index-signature types fail closed and resolve to `never`. - Mixed types are driven off the explicit-key subset, so index signatures no longer mask `HasRequiredKeys` / `RequireAtLeastOne`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1dd996796
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| keyof OmitIndexSignature<T> extends never | ||
| ? never |
There was a problem hiding this comment.
Preserve
any inputs in NonEmptyObject
When callers instantiate NonEmptyObject<any> (or a generic that resolves to any), this new guard treats any like a pure index-signature type: OmitIndexSignature<any> maps over string | number | symbol and removes every key, so the conditional resolves to never. That regresses from the previous definition, which preserved any, and can turn permissive downstream generic APIs into impossible types.
Useful? React with 👍 / 👎.
|
I think this accidentally changes The new explicit-key guard treats I manually checked this with expectType<any>({} as NonEmptyObject<any>);Can you special-case |
Addresses #821.
NonEmptyObjectcurrently accepts{}for types whose only members are index signatures:Why
A type whose only members are index signatures cannot statically express "at least one dynamic key" in TypeScript. The previous definition fell through to
RequireAtLeastOne<T, keyof T>, which forkeyof T = stringmapped back to an index signature and trivially accepted{}.What changed
Re-derive the key set via
OmitIndexSignatureso:never.HasRequiredKeys/RequireAtLeastOneoperate on the explicit-key subset, so index signatures no longer mask non-emptiness enforcement.Tests
Added to
test-d/non-empty-object.ts:never.never.NonEmptyObjectfails for objects with dynamic properties #821 repro ({foo: {}}) is rejected.npm testpasses locally.