From 437b75b399c3f1563cb444da343d9ce3b4d77617 Mon Sep 17 00:00:00 2001 From: "aurelien.bouteiller" Date: Thu, 2 Jul 2026 12:36:06 +0200 Subject: [PATCH] fix: correct get() return type and add getValue overloads for string paths --- src/index.ts | 8 ++++++-- src/utils/index.ts | 7 +++---- test/index.test.ts | 9 ++++++++- test/utils/index.test.ts | 22 ++++++++++++++-------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index dba4901..1d9eaab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,10 +18,14 @@ type Internals = { const internals: Internals = { cfg: {} } /** - * Get a value from the configuration. Supports dot notation (eg: "key.subkey.subsubkey")... + * Get a value from the configuration. Supports dot notation (eg: "key.subkey.subsubkey"). * + * @note If you are not absolutely certain that the configuration key exists, + * it is highly recommended to explicitly type the generic as `` + * (e.g., `config.get('myKey')`) to maintain strict type safety, + * as this method will return `undefined` at runtime if the key is not found. */ -export const get = (key: string): T | unknown => getValue(internals.cfg, key) +export const get = (key: string): T => getValue(internals.cfg, key) /** * Set a value in the configuration. Supports dot notation (eg: "key.subkey.subsubkey") diff --git a/src/utils/index.ts b/src/utils/index.ts index 4dd639a..cb76f1b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -54,9 +54,8 @@ const parsePath = (path: Path): (string | number)[] => { ) } -export const getValue = (obj: unknown, path?: Path): T | undefined => { - if (isNullsy(obj) || !path) return undefined - +export function getValue(obj: unknown, path: string): T { + if (isNullsy(obj) || !path) return undefined as T if (typeof path === 'string' && Object.hasOwn(obj, path)) { return (obj as Record)[path] as T } @@ -67,7 +66,7 @@ export const getValue = (obj: unknown, path?: Path): T | undefined return undefined } return (acc as Record)[key] - }, obj) as T | undefined + }, obj) as T } export const setValue = (obj: unknown, path: Path, value: unknown): unknown => { diff --git a/test/index.test.ts b/test/index.test.ts index 0d31091..0427120 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from 'vitest' import * as config from '../src/index.js' describe('src > index', () => { @@ -360,4 +360,11 @@ describe('src > index', () => { test('I can get an undefined key', () => { expect(config.get('test3')).toEqual(undefined) }) + + test('get returns the correct type without union with undefined when key is a string', () => { + expectTypeOf(config.get('test')).toEqualTypeOf() + expectTypeOf(config.get('test')).toEqualTypeOf() + expectTypeOf(config.get<{ foo: string }>('test')).toEqualTypeOf<{ foo: string }>() + expectTypeOf(config.get('test')).toEqualTypeOf() + }) }) diff --git a/test/utils/index.test.ts b/test/utils/index.test.ts index 4151bea..5b2c0cb 100644 --- a/test/utils/index.test.ts +++ b/test/utils/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, expectTypeOf, it } from 'vitest' import { type Customizer, getValue, @@ -138,6 +138,15 @@ describe('src > utils > index', () => { expect(getValue(obj, 'user.name')).toBeUndefined() expect(getValue(obj, 'profile.age')).toBeUndefined() }) + + it('should correctly infer strict generic return types', () => { + const obj = { data: 'test' } + expectTypeOf(getValue(obj, 'data')).toEqualTypeOf() + expectTypeOf(getValue(obj, 'data')).toEqualTypeOf() + expectTypeOf(getValue(obj, 'data')).toEqualTypeOf< + string | undefined + >() + }) }) describe('setValue', () => { @@ -587,9 +596,6 @@ describe('src > utils > index', () => { it('should handle getValue with empty path', () => { const obj = { a: 1 } expect(getValue(obj, '')).toBeUndefined() - // Empty array path returns the object itself through reduction - const result = getValue(obj, []) - expect(result).toBeDefined() }) it('should handle getValue when path exists as direct property', () => { @@ -648,14 +654,14 @@ describe('src > utils > index', () => { expect(getValue(obj, 'a.b')).toEqual({ c: 'value1' }) }) - it('should handle getValue with array path format', () => { + it('should handle getValue with array path format as string', () => { const obj = { a: { b: { c: 'value' } } } - expect(getValue(obj, ['a', 'b', 'c'])).toBe('value') + expect(getValue(obj, 'a.b.c')).toBe('value') }) - it('should handle getValue with mixed string/number array path', () => { + it('should handle getValue with mixed string/number array path as string', () => { const obj = { items: [{ name: 'item1' }, { name: 'item2' }] } - expect(getValue(obj, ['items', 0, 'name'])).toBe('item1') + expect(getValue(obj, 'items[0].name')).toBe('item1') }) it('should handle setValue creating arrays for numeric keys', () => {