From cb4ee0e92cd3bff8353bb6738bd1f54db87dc293 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 8 Apr 2026 19:53:46 +0700 Subject: [PATCH] Fix `isEnumCase` incorrectly accepting numeric enum key names --- source/index.ts | 19 ++++++++++++++++--- test/test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/source/index.ts b/source/index.ts index 1a9308d..6104501 100644 --- a/source/index.ts +++ b/source/index.ts @@ -542,8 +542,21 @@ export function isEmptyStringOrWhitespace(value: unknown): value is '' | Whitesp } export function isEnumCase(value: unknown, targetEnum: T): value is T[keyof T] { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return Object.values(targetEnum as any).includes(value as string); + // Numeric enums have reverse mappings (e.g. `Direction[0] = "Up"`), so their runtime object contains both `{ Up: 0 }` and `{ "0": "Up" }`. Filtering out entries that round-trip like a canonical number and point back to an own property leaves only actual enum member values. + const enumObject = targetEnum as Record; + + return Object.entries(enumObject).some(([key, enumValue]) => { + if (!isString(enumValue)) { + return enumValue === value; + } + + const numericKey = Number(key); + if (Number.isNaN(numericKey) || String(numericKey) !== key) { + return enumValue === value; + } + + return enumValue === value && !(Object.hasOwn(enumObject, enumValue) && enumObject[enumValue] === numericKey); + }); } export function isError(value: unknown): value is Error { @@ -786,7 +799,7 @@ export function isPromise(value: unknown): value is Promise { return isNativePromise(value) || hasPromiseApi(value); } -// `PropertyKey` is any value that can be used as an object key (string, number, or symbol) +// `PropertyKey` is any value that can be used as an object key (string, number, or symbol). Note: NaN is technically `typeof 'number'` and thus fits TypeScript's `PropertyKey`, but we intentionally exclude it here because using NaN as a property key is almost always a mistake. export function isPropertyKey(value: unknown): value is PropertyKey { return isAny([isString, isNumber, isSymbol], value); } diff --git a/test/test.ts b/test/test.ts index 11ab0df..ce26b50 100644 --- a/test/test.ts +++ b/test/test.ts @@ -855,6 +855,12 @@ test('is.enumCase', () => { Key2 = 'key2', } + enum NumericKeyStringEnum { + // eslint-disable-next-line @stylistic/quote-props + '0' = 'zero', + '01' = 'padded', + } + assert.ok(is.enumCase('key1', NonNumericalEnum)); assert.doesNotThrow(() => { isAssert.enumCase('key1', NonNumericalEnum); @@ -864,6 +870,40 @@ test('is.enumCase', () => { assert.throws(() => { isAssert.enumCase('invalid', NonNumericalEnum); }); + + assert.ok(is.enumCase('zero', NumericKeyStringEnum)); + assert.ok(is.enumCase('padded', NumericKeyStringEnum)); + assert.doesNotThrow(() => { + isAssert.enumCase('zero', NumericKeyStringEnum); + }); + assert.doesNotThrow(() => { + isAssert.enumCase('padded', NumericKeyStringEnum); + }); + + enum NumericalEnum { + Key1 = 0, + Key2 = 1, + } + + assert.ok(is.enumCase(0, NumericalEnum)); + assert.ok(is.enumCase(1, NumericalEnum)); + assert.strictEqual(is.enumCase('Key1', NumericalEnum), false); + assert.strictEqual(is.enumCase('Key2', NumericalEnum), false); + assert.doesNotThrow(() => { + isAssert.enumCase(0, NumericalEnum); + }); + assert.throws(() => { + isAssert.enumCase('Key1', NumericalEnum); + }); + + enum HeterogeneousEnum { + A = 1, + B = 'hello', + } + + assert.ok(is.enumCase(1, HeterogeneousEnum)); + assert.ok(is.enumCase('hello', HeterogeneousEnum)); + assert.strictEqual(is.enumCase('A', HeterogeneousEnum), false); }); test('is.directInstanceOf', () => {