Skip to content

Commit

Permalink
feat(types): At
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed May 22, 2023
1 parent e1501e5 commit a725846
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 16 deletions.
67 changes: 67 additions & 0 deletions src/types/__tests__/at.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @file Type Tests - At
* @module tutils/types/tests/unit-d/At
*/

import type Author from '#fixtures/author.interface'
import type TestSubject from '../at'
import type EmptyArray from '../empty-array'
import type EmptyString from '../empty-string'

describe('unit-d:types/At', () => {
type F = undefined

it('should equal F if K is out of range', () => {
// Arrange
type T1 = ['a', 'b', 'c']
type T2 = 'abc'

// Expect
expectTypeOf<TestSubject<T1, '-4'>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T1, '3'>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T1, -4>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T1, 3>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T2, '-4'>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T2, '3'>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T2, -4>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<T2, 3>>().toEqualTypeOf<F>()
})

it('should equal F if T extends EmptyString', () => {
expectTypeOf<TestSubject<EmptyString, 0>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<Readonly<EmptyString>, 0>>().toEqualTypeOf<F>()
})

it('should equal F if T extends EmptyArray', () => {
expectTypeOf<TestSubject<EmptyArray, 0>>().toEqualTypeOf<F>()
expectTypeOf<TestSubject<EmptyArray, 0>>().toEqualTypeOf<F>()
})

it('should equal F | T[number] if T is not string literal or tuple', () => {
// Arrange
type T1 = Author[]
type T2 = string

// Expect
expectTypeOf<TestSubject<T1, '0'>>().toEqualTypeOf<F | T1[number]>()
expectTypeOf<TestSubject<T1, 0>>().toEqualTypeOf<F | T1[number]>()
expectTypeOf<TestSubject<T2, '1'>>().toEqualTypeOf<F | T2[number]>()
expectTypeOf<TestSubject<T2, 1>>().toEqualTypeOf<F | T2[number]>()
})

it('should equal T.at(K)! if T is string literal or tuple', () => {
// Arrange
type T1 = [Author, Author]
type T2 = 'def'

// Expect
expectTypeOf<TestSubject<T1, '1'>>().toEqualTypeOf<Author>()
expectTypeOf<TestSubject<T1, '-1'>>().toEqualTypeOf<Author>()
expectTypeOf<TestSubject<T1, 1>>().toEqualTypeOf<Author>()
expectTypeOf<TestSubject<T1, -1>>().toEqualTypeOf<Author>()
expectTypeOf<TestSubject<T2, '1'>>().toEqualTypeOf<'e'>()
expectTypeOf<TestSubject<T2, '-1'>>().toEqualTypeOf<'d' | 'e' | 'f'>()
expectTypeOf<TestSubject<T2, 1>>().toEqualTypeOf<'e'>()
expectTypeOf<TestSubject<T2, -1>>().toEqualTypeOf<'d' | 'e' | 'f'>()
})
})
10 changes: 8 additions & 2 deletions src/types/__tests__/empty-array.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import type TestSubject from '../empty-array'

describe('unit-d:types/EmptyArray', () => {
it('should equal type of []', () => {
expectTypeOf<TestSubject>().toEqualTypeOf<[]>()
it('should extract Readonly<[]>', () => {
expectTypeOf<TestSubject>().extract<Readonly<[]>>().not.toBeNever()
expectTypeOf<Readonly<[]>>().toMatchTypeOf<TestSubject>()
})

it('should extract []', () => {
expectTypeOf<TestSubject>().extract<[]>().not.toBeNever()
expectTypeOf<[]>().toMatchTypeOf<TestSubject>()
})
})
2 changes: 1 addition & 1 deletion src/types/__tests__/empty-string.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type TestSubject from '../empty-string'

describe('unit-d:types/EmptyString', () => {
it('should equal type of ""', () => {
it('should equal ""', () => {
expectTypeOf<TestSubject>().toEqualTypeOf<''>()
})
})
12 changes: 8 additions & 4 deletions src/types/__tests__/numeric.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import type TestSubject from '../numeric'

describe('unit-d:types/Numeric', () => {
it('should allow value that is stringified number', () => {
assertType<TestSubject>(`${faker.number.int()}`)
it('should equal `${number}`', () => {
expectTypeOf<TestSubject>().toEqualTypeOf<`${number}`>()
})

it('should not allow value that is not stringified number', () => {
expectTypeOf<TestSubject>().not.toEqualTypeOf(faker.string.uuid())
it('should match positive numeric', () => {
expectTypeOf<'13'>().toMatchTypeOf<TestSubject>()
})

it('should match negative numeric', () => {
expectTypeOf<'-13'>().toMatchTypeOf<TestSubject>()
})
})
15 changes: 11 additions & 4 deletions src/types/__tests__/split.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
* @module tutils/types/tests/unit-d/Split
*/

import type EmptyArray from '../empty-array'
import type EmptyString from '../empty-string'
import type TestSubject from '../split'

describe('unit-d:types/Split', () => {
it('should equal T.split(Delimiter)', () => {
// Arrange
type T = 'publisher.email'
type T1 = EmptyString
type T2 = 'publisher.email'

// Expect
expectTypeOf<TestSubject<T, undefined>>().toEqualTypeOf<[T]>()
expectTypeOf<TestSubject<T, RegExp>>().toEqualTypeOf<string[]>()
expectTypeOf<TestSubject<T, '.'>>().toEqualTypeOf<['publisher', 'email']>()
expectTypeOf<TestSubject<T1>>().toEqualTypeOf<[T1]>()
expectTypeOf<TestSubject<T1, EmptyString>>().toEqualTypeOf<EmptyArray>()
expectTypeOf<TestSubject<T1, RegExp>>().toEqualTypeOf<EmptyArray>()
expectTypeOf<TestSubject<T1, '.'>>().toEqualTypeOf<[T1]>()
expectTypeOf<TestSubject<T2>>().toEqualTypeOf<[T2]>()
expectTypeOf<TestSubject<T2, RegExp>>().toEqualTypeOf<string[]>()
expectTypeOf<TestSubject<T2, '.'>>().toEqualTypeOf<['publisher', 'email']>()
})
})
75 changes: 75 additions & 0 deletions src/types/at.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @file Type Definitions - At
* @module tutils/types/At
*/

import type EmptyArray from './empty-array'
import type EmptyString from './empty-string'
import type IfTuple from './if-tuple'
import type NumberString from './number-string'
import type Numeric from './numeric'
import type Split from './split'
import type TupleLength from './tuple-length'

/**
* Constructs a union of numbers in the range `[0,Max)`.
*
* @internal
*
* @template Max - Upper bound of range (exclusive)
* @template Acc - Accumulator
*/
type Enumerate<
Max extends number,
Acc extends readonly number[] = EmptyArray
> = Acc['length'] extends Max
? Acc[number]
: Enumerate<Max, [...Acc, Acc['length']]>

/**
* Constructs a union of numerics in the range `[-1 * Max, Max)`.
*
* @internal
*
* @template Max - Upper bound of range (exclusive)
*/
type Range<Max extends number> =
| Exclude<`-${Exclude<Enumerate<Max>, Enumerate<0>>}`, `-${0}`>
| `-${Max}`
| `${Exclude<Enumerate<Max>, Enumerate<0>>}`

/**
* Indexes `T` at `K`.
*
* Partially supports negative indices.
*
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/at
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/at
*
* @template T - Type to evaluate
* @template K - Index
* @template F - Fallback value
*/
type At<
T extends string | readonly unknown[],
K extends NumberString,
F = undefined
> = NonNullable<T> extends EmptyArray | EmptyString
? F
: K extends Numeric | number
? NonNullable<T> extends string
? [string] extends [NonNullable<T>]
? F | T[number]
: Split<NonNullable<T>, ''> extends infer B
? `${K}` extends Range<TupleLength<B>>
? B[K & keyof B]
: F
: F
: IfTuple<
NonNullable<T>,
`${K}` extends Range<TupleLength<NonNullable<T>>> ? T[K & keyof T] : F,
F | T[number]
>
: F

export type { At as default }
2 changes: 1 addition & 1 deletion src/types/empty-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
/**
* An empty array.
*/
type EmptyArray = []
type EmptyArray = Readonly<[]> | []

export type { EmptyArray as default }
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/types
*/

export type { default as At } from './at'
export type { default as Booleanish } from './booleanish'
export type { default as BuiltIn } from './built-in'
export type { default as Class } from './class'
Expand Down
3 changes: 2 additions & 1 deletion src/types/numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/

/**
* A string that contains only numbers.
* A string that contains only numbers (not including the leading `-` if the
* numeric is negative).
*/
type Numeric = `${number}`

Expand Down
12 changes: 9 additions & 3 deletions src/types/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@
* @module tutils/types/Split
*/

import type EmptyArray from './empty-array'
import type EmptyString from './empty-string'
import type EnsureString from './ensure-string'

/**
* Splits string `S` using the given `Delimiter`.
*
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/split
*
* @template T - String to split
* @template Delimiter - String delimiter
*/
type Split<
T extends string,
Delimiter extends RegExp | string | undefined
Delimiter extends RegExp | string | undefined = undefined
> = RegExp extends Delimiter
? string[]
? T extends EmptyString
? EmptyArray
: string[]
: T extends `${infer Head}${EnsureString<Delimiter>}${infer Tail}`
? [Head, ...Split<Tail, Delimiter>]
: T extends Delimiter
? []
? EmptyArray
: [T]

export type { Split as default }

0 comments on commit a725846

Please sign in to comment.