Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<T | undefined>`
* (e.g., `config.get<string | undefined>('myKey')`) to maintain strict type safety,
* as this method will return `undefined` at runtime if the key is not found.
*/
export const get = <T>(key: string): T | unknown => getValue(internals.cfg, key)
export const get = <T = unknown>(key: string): T => getValue<T>(internals.cfg, key)
Comment thread
tduyng marked this conversation as resolved.

/**
* Set a value in the configuration. Supports dot notation (eg: "key.subkey.subsubkey")
Expand Down
7 changes: 3 additions & 4 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ const parsePath = (path: Path): (string | number)[] => {
)
}

export const getValue = <T = unknown>(obj: unknown, path?: Path): T | undefined => {
if (isNullsy(obj) || !path) return undefined

export function getValue<T = unknown>(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<string, unknown>)[path] as T
}
Expand All @@ -67,7 +66,7 @@ export const getValue = <T = unknown>(obj: unknown, path?: Path): T | undefined
return undefined
}
return (acc as Record<string, unknown>)[key]
}, obj) as T | undefined
}, obj) as T
}

export const setValue = (obj: unknown, path: Path, value: unknown): unknown => {
Expand Down
9 changes: 8 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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<unknown>()
expectTypeOf(config.get<string>('test')).toEqualTypeOf<string>()
expectTypeOf(config.get<{ foo: string }>('test')).toEqualTypeOf<{ foo: string }>()
expectTypeOf(config.get<string | undefined>('test')).toEqualTypeOf<string | undefined>()
})
})
22 changes: 14 additions & 8 deletions test/utils/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, expectTypeOf, it } from 'vitest'
import {
type Customizer,
getValue,
Expand Down Expand Up @@ -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<unknown>()
expectTypeOf(getValue<string>(obj, 'data')).toEqualTypeOf<string>()
expectTypeOf(getValue<string | undefined>(obj, 'data')).toEqualTypeOf<
string | undefined
>()
})
})

describe('setValue', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down