diff --git a/src/types/__tests__/at.spec-d.ts b/src/types/__tests__/at.spec-d.ts new file mode 100644 index 00000000..bd76906c --- /dev/null +++ b/src/types/__tests__/at.spec-d.ts @@ -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>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + it('should equal F if T extends EmptyString', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf, 0>>().toEqualTypeOf() + }) + + it('should equal F if T extends EmptyArray', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + it('should equal F | T[number] if T is not string literal or tuple', () => { + // Arrange + type T1 = Author[] + type T2 = string + + // Expect + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + it('should equal T.at(K)! if T is string literal or tuple', () => { + // Arrange + type T1 = [Author, Author] + type T2 = 'def' + + // Expect + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<'e'>() + expectTypeOf>().toEqualTypeOf<'d' | 'e' | 'f'>() + expectTypeOf>().toEqualTypeOf<'e'>() + expectTypeOf>().toEqualTypeOf<'d' | 'e' | 'f'>() + }) +}) diff --git a/src/types/__tests__/empty-array.spec-d.ts b/src/types/__tests__/empty-array.spec-d.ts index 82c5db84..635b5908 100644 --- a/src/types/__tests__/empty-array.spec-d.ts +++ b/src/types/__tests__/empty-array.spec-d.ts @@ -6,7 +6,13 @@ import type TestSubject from '../empty-array' describe('unit-d:types/EmptyArray', () => { - it('should equal type of []', () => { - expectTypeOf().toEqualTypeOf<[]>() + it('should extract Readonly<[]>', () => { + expectTypeOf().extract>().not.toBeNever() + expectTypeOf>().toMatchTypeOf() + }) + + it('should extract []', () => { + expectTypeOf().extract<[]>().not.toBeNever() + expectTypeOf<[]>().toMatchTypeOf() }) }) diff --git a/src/types/__tests__/empty-string.spec-d.ts b/src/types/__tests__/empty-string.spec-d.ts index 2c64676a..474d6b72 100644 --- a/src/types/__tests__/empty-string.spec-d.ts +++ b/src/types/__tests__/empty-string.spec-d.ts @@ -6,7 +6,7 @@ import type TestSubject from '../empty-string' describe('unit-d:types/EmptyString', () => { - it('should equal type of ""', () => { + it('should equal ""', () => { expectTypeOf().toEqualTypeOf<''>() }) }) diff --git a/src/types/__tests__/numeric.spec-d.ts b/src/types/__tests__/numeric.spec-d.ts index 28b188fa..2a77e37e 100644 --- a/src/types/__tests__/numeric.spec-d.ts +++ b/src/types/__tests__/numeric.spec-d.ts @@ -6,11 +6,15 @@ import type TestSubject from '../numeric' describe('unit-d:types/Numeric', () => { - it('should allow value that is stringified number', () => { - assertType(`${faker.number.int()}`) + it('should equal `${number}`', () => { + expectTypeOf().toEqualTypeOf<`${number}`>() }) - it('should not allow value that is not stringified number', () => { - expectTypeOf().not.toEqualTypeOf(faker.string.uuid()) + it('should match positive numeric', () => { + expectTypeOf<'13'>().toMatchTypeOf() + }) + + it('should match negative numeric', () => { + expectTypeOf<'-13'>().toMatchTypeOf() }) }) diff --git a/src/types/__tests__/split.spec-d.ts b/src/types/__tests__/split.spec-d.ts index e50a4b4b..a1e2f879 100644 --- a/src/types/__tests__/split.spec-d.ts +++ b/src/types/__tests__/split.spec-d.ts @@ -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>().toEqualTypeOf<[T]>() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf<['publisher', 'email']>() + expectTypeOf>().toEqualTypeOf<[T1]>() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<[T1]>() + expectTypeOf>().toEqualTypeOf<[T2]>() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<['publisher', 'email']>() }) }) diff --git a/src/types/at.ts b/src/types/at.ts new file mode 100644 index 00000000..13e5748f --- /dev/null +++ b/src/types/at.ts @@ -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 + +/** + * Constructs a union of numerics in the range `[-1 * Max, Max)`. + * + * @internal + * + * @template Max - Upper bound of range (exclusive) + */ +type Range = + | Exclude<`-${Exclude, Enumerate<0>>}`, `-${0}`> + | `-${Max}` + | `${Exclude, 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 extends EmptyArray | EmptyString + ? F + : K extends Numeric | number + ? NonNullable extends string + ? [string] extends [NonNullable] + ? F | T[number] + : Split, ''> extends infer B + ? `${K}` extends Range> + ? B[K & keyof B] + : F + : F + : IfTuple< + NonNullable, + `${K}` extends Range>> ? T[K & keyof T] : F, + F | T[number] + > + : F + +export type { At as default } diff --git a/src/types/empty-array.ts b/src/types/empty-array.ts index 40fd132b..be332c9d 100644 --- a/src/types/empty-array.ts +++ b/src/types/empty-array.ts @@ -6,6 +6,6 @@ /** * An empty array. */ -type EmptyArray = [] +type EmptyArray = Readonly<[]> | [] export type { EmptyArray as default } diff --git a/src/types/index.ts b/src/types/index.ts index f6492343..339be04e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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' diff --git a/src/types/numeric.ts b/src/types/numeric.ts index 7de887d2..444643cf 100644 --- a/src/types/numeric.ts +++ b/src/types/numeric.ts @@ -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}` diff --git a/src/types/split.ts b/src/types/split.ts index 1192ec22..591a1c9c 100644 --- a/src/types/split.ts +++ b/src/types/split.ts @@ -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}${infer Tail}` ? [Head, ...Split] : T extends Delimiter - ? [] + ? EmptyArray : [T] export type { Split as default }