Skip to content

Commit

Permalink
feat(utils): get
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed May 26, 2023
1 parent a725570 commit 87b7a09
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 7 deletions.
41 changes: 41 additions & 0 deletions __fixtures__/person.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @file Test Fixtures - Person
* @module tests/fixtures/Person
*/

/**
* Object representing a person.
*/
interface Person {
/**
* Person's age.
*/
age: number

/**
* Person's friends.
*/
friends?: Person[]

/**
* Object representing a person's name.
*/
name: {
/**
* First name.
*/
first: string

/**
* Last name.
*/
last: string

/**
* Middle name.
*/
middle?: string
}
}

export type { Person as default }
19 changes: 12 additions & 7 deletions src/types/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type EmptyObject from './empty-object'
import type EmptyString from './empty-string'
import type Fallback from './fallback'
import type IfEqual from './if-equal'
import type IfUndefined from './if-undefined'
import type IndexSignature from './index-signature'
import type NumberString from './number-string'
import type Numeric from './numeric'
Expand All @@ -32,14 +33,18 @@ type Get<T, K extends IndexSignature, F = undefined> = T extends
: K extends `${infer H extends NumberString}.${infer Tail}`
? NonNullable<T> extends string | readonly unknown[]
? H extends Numeric | number
? undefined extends At<NonNullable<T>, H>
? F
: IfEqual<
At<NonNullable<T>, H>,
NonNullable<At<NonNullable<T>, H>>,
Get<NonNullable<T>[H & keyof NonNullable<T>], Tail, F>,
F | Get<NonNullable<T>[H & keyof NonNullable<T>], Tail, F>
? At<NonNullable<T>, H> extends infer U
? IfUndefined<
U,
F,
IfEqual<
U,
NonNullable<U>,
Get<NonNullable<T>[H & keyof NonNullable<T>], Tail, F>,
F | Get<NonNullable<T>[H & keyof NonNullable<T>], Tail, F>
>
>
: never
: never
: H extends keyof NonNullable<T>
? IfEqual<
Expand Down
22 changes: 22 additions & 0 deletions src/utils/__tests__/get.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @file Type Tests - get
* @module tutils/utils/tests/unit-d/get
*/

import type Person from '#fixtures/person.interface'
import type { EmptyString, Get } from '#src/types'
import type testSubject from '../get'

describe('unit-d:utils/get', () => {
it('should return Get<T, P, F>', () => {
// Arrange
type T = Person
type P = 'friends.0.name.middle'
type F = EmptyString

// Expect
expectTypeOf<typeof testSubject<T, P, F>>().returns.toEqualTypeOf<
Get<T, P, F>
>()
})
})
62 changes: 62 additions & 0 deletions src/utils/__tests__/get.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @file Unit Tests - get
* @module tutils/utils/tests/unit/get
*/

import type Person from '#fixtures/person.interface'
import type { EmptyString } from '#src/types'
import testSubject from '../get'

describe('unit:utils/get', () => {
let person: Person

beforeAll(() => {
person = {
age: faker.number.int({ max: 25, min: 18 }),
friends: [
{
age: faker.number.int({ max: 25, min: 18 }),
name: {
first: faker.person.firstName(),
last: faker.person.lastName()
}
}
],
name: {
first: faker.person.firstName(),
last: faker.person.lastName(),
middle: faker.person.middleName()
}
}
})

it('should return fallback if indexed value is undefined', () => {
// Arrange
const fallback: EmptyString = ''

// Act
const result = testSubject(person, 'friends.0.name.middle', fallback)

// Expect
expect(result).to.deep.equal(fallback)
})

it('should return dynamically indexed value', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, unknown][] = [
['person', 0, undefined, 'p'],
[null, 'age', undefined, null],
[person, 'age', undefined, person.age],
[person, 'age.', undefined, person.age],
[person, 'friends', undefined, person.friends],
[person, 'friends.0.name.last', undefined, person.friends![0]!.name.last],
[person, 'name.first', undefined, person.name.first],
[person, 'name.first.-1', undefined, person.name.first.at(-1)]
]

// Act + Expect
cases.forEach(([value, path, fallback, expected]) => {
expect(testSubject(value, path, fallback)).to.deep.equal(expected)
})
})
})
74 changes: 74 additions & 0 deletions src/utils/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @file Utilities - get
* @module tutils/utils/get
*/

import type { Get, NumberString } from '#src/types'
import at from './at'
import cast from './cast'
import isArray from './is-array'
import isEmptyString from './is-empty-string'
import isNIL from './is-nil'
import isNumeric from './is-numeric'
import isString from './is-string'
import isUndefined from './is-undefined'
import trim from './trim'

/**
* Dynamically indexes `data` at `path`.
*
* If the indexed value is `undefined`, `fallback` will be returned instead.
*
* Supports dot-notation for nested paths and array indexing.
*
* @template T - Value to index
* @template P - Index path
* @template F - Fallback value type
*
* @param {T} data - Value to index
* @param {P} path - Index path
* @param {F} [fallback] - Fallback value
* @return {Get<T, P, F>} Dynamically indexed value or `fallback`
*/
function get<T, P extends NumberString, F = undefined>(
data: T,
path: P,
fallback?: F
): Get<T, P, F> {
/**
* Path segments.
*
* @const {string[]} segments
*/
const segments: string[] = `${path}`.split(/[.[\]]/g).map(trim)

/**
* Dynamically indexed value.
*
* @var {unknown} value
*/
let value: unknown = data

// dynamically index data
for (const key of segments) {
// exit early if indexed value is null or undefined
if (isNIL(value)) break

// do nothing if key is an empty string
if (isEmptyString(key)) continue

// reset indexed value
switch (true) {
case (isArray(value) || isString(value)) && isNumeric(key):
value = at(cast<string | readonly unknown[]>(value), +key)
break
default:
value = cast<Record<string, any>>(value)[key]
break
}
}

return (isUndefined(value) ? fallback : value) as Get<T, P, F>
}

export default get
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as count } from './count'
export { default as diff } from './diff'
export { default as equal } from './equal'
export { default as fork } from './fork'
export { default as get } from './get'
export { default as group } from './group'
export { default as includes } from './includes'
export { default as intersection } from './intersection'
Expand Down

0 comments on commit 87b7a09

Please sign in to comment.