Skip to content

Commit

Permalink
feat(types): Spread
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 31, 2023
1 parent 6b2a41a commit a6db57f
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 21 deletions.
12 changes: 6 additions & 6 deletions src/types/__tests__/indices.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ describe('unit-d:types/Indices', () => {
: never
: never

it('should equal number if T is any', () => {
expectTypeOf<TestSubject<any>>().toBeNumber()
})

it('should equal never if T is never', () => {
expectTypeOf<TestSubject<never>>().toBeNever()
})

it('should equal number if T is any', () => {
expectTypeOf<TestSubject<any>>().toBeNumber()
})

describe('T extends string', () => {
it('should equal never if T extends EmptyString', () => {
expectTypeOf<TestSubject<EmptyString>>().toBeNever()
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('unit-d:types/Indices', () => {
describe('T extends readonly [T[0], ...T[number][]]', () => {
it('should construct union of negative and positive indices', () => {
// Arrange
type T = ['a', 'b'?]
type T = readonly ['a', 'b'?]

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<IndicesRange<T>>()
Expand All @@ -73,7 +73,7 @@ describe('unit-d:types/Indices', () => {

describe('number extends Length<T>', () => {
it('should equal number', () => {
expectTypeOf<TestSubject<string[]>>().toEqualTypeOf<number>()
expectTypeOf<TestSubject<readonly string[]>>().toEqualTypeOf<number>()
})
})
})
Expand Down
201 changes: 201 additions & 0 deletions src/types/__tests__/spread.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* @file Type Tests - Spread
* @module tutils/types/tests/unit-d/Spread
*/

import type Vehicle from '#fixtures/types/vehicle'
import type At from '../at'
import type EmptyObject from '../empty-object'
import type Fn from '../fn'
import type Nilable from '../nilable'
import type Nullable from '../nullable'
import type Objectify from '../objectify'
import type Optional from '../optional'
import type TestSubject from '../spread'
import type Writable from '../writable'

describe('unit-d:types/Spread', () => {
it('should equal Objectify<T> if T is any', () => {
// Arrange
type T = any

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Objectify<T>>()
})

it('should equal Objectify<T> if T is never', () => {
// Arrange
type T = never

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Objectify<T>>()
})

it('should equal Objectify<T> if T is unknown', () => {
// Arrange
type T = unknown

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Objectify<T>>()
})

describe('T extends ObjectCurly', () => {
describe('EmptyObject extends T', () => {
it('should equal {}', () => {
expectTypeOf<TestSubject<{}>>().toEqualTypeOf<{}>()
expectTypeOf<TestSubject<EmptyObject>>().toEqualTypeOf<{}>()
})
})

describe('Simplify<T> extends ObjectPlain', () => {
it('should equal Writable<T>', () => {
// Arrange
type T = Readonly<Vehicle>

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Writable<T>>()
})
})

describe('T extends Readonly<BuiltIn>', () => {
it('should enumerate properties of T', () => {
// Arrange
type T = Map<Vehicle['vin'], Vehicle> & { readonly id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})
})

describe('T extends Primitive', () => {
describe('T extends NIL', () => {
it('should equal Objectify<T>', () => {
// Arrange
type T1 = null
type T2 = undefined

// Expect
expectTypeOf<TestSubject<T1>>().toEqualTypeOf<Objectify<T1>>()
expectTypeOf<TestSubject<T2>>().toEqualTypeOf<Objectify<T2>>()
})
})

describe('T extends bigint', () => {
it('should spread T into object', () => {
// Arrange
type T = bigint & { readonly id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})

describe('T extends boolean', () => {
it('should spread T into object', () => {
// Arrange
type T = false & { readonly id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})

describe('T extends number', () => {
it('should spread T into object', () => {
// Arrange
type T = number & { readonly id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})

describe('T extends string', () => {
describe('IsLiteral<T> extends true', () => {
it('should spread T into object', () => {
// Arrange
type T = 'vin'

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{
[x: number]: At<T, number>
'0': At<T, 0>
'1': At<T, 1>
'2': At<T, 2>
}>()
})
})

describe('number extends Length<T>', () => {
it('should spread T into object', () => {
// Arrange
type T = Vehicle['vin'] & { readonly id: string }
type Expect = { [x: number]: Optional<string>; id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Expect>()
})
})
})

describe('T extends symbol', () => {
it('should spread T into object', () => {
// Arrange
type T = symbol & { readonly id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})
})

describe('T extends Readonly<Fn>', () => {
it('should spread T into object', () => {
// Arrange
type T = Readonly<Fn & { id: string }>

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{ id: string }>()
})
})

describe('T extends readonly unknown[]', () => {
describe('IsTuple<T> extends true', () => {
it('should spread T into object', () => {
// Arrange
type T = readonly [Vehicle, Nilable<Vehicle>, Nullable<Vehicle>?]

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<{
[x: number]: At<T, number>
'0': At<T, 0>
'1': At<T, 1>
'2'?: At<T, 2, never>
}>()
})
})

describe('number extends Length<T>', () => {
it('should spread T into object', () => {
// Arrange
type T = readonly Vehicle[] & { readonly id: string }
type Expect = { [x: number]: Optional<Vehicle>; id: string }

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Expect>()
})
})
})

describe('unions', () => {
it('should distribute over unions', () => {
// Arrange
type T = Nullable<['vin']>
type Expect = { [x: number]: 'vin'; '0': 'vin' } | {}

// Expect
expectTypeOf<TestSubject<T>>().toEqualTypeOf<Expect>()
})
})
})
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export type { default as Shake } from './shake'
export type { default as Sift } from './sift'
export type { default as Simplify } from './simplify'
export type { default as Split } from './split'
export type { default as Spread } from './spread'
export type { default as Stringafiable } from './stringafiable'
export type { default as Stringify } from './stringify'
export type { default as Tail } from './tail'
Expand Down
28 changes: 14 additions & 14 deletions src/types/indices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,22 @@ import type UnwrapNumeric from './unwrap-numeric'
*
* @template T - Type to evaluate
*/
type Indices<T extends string | readonly unknown[]> = (
T extends EmptyString | Readonly<EmptyArray>
? never
: Length<T> extends infer L extends number
? number extends L
? L
: NaturalRange<L> extends infer R extends number
? Length<Required<T>> extends infer L extends number
? {
[K in R]: K | UnwrapNumeric<Exclude<`-${Subtract<L, K>}`, '-0'>>
}[R]
: never
type Indices<T extends { length: number }> = T extends
| EmptyString
| Readonly<EmptyArray>
? never
: Length<T> extends infer L extends number
? number extends L
? L
: NaturalRange<L> extends infer R extends number
? {
[K in R]:
| K
| UnwrapNumeric<Exclude<`-${Subtract<Length<Required<T>>, K>}`, '-0'>>
}[R] extends infer I extends number
? I
: never
: never
) extends infer I extends number
? I
: never

export type { Indices as default }
2 changes: 1 addition & 1 deletion src/types/length.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type Split from './split'
*
* @template T - Type to evaluate
*/
type Length<T extends { readonly length: number }> =
type Length<T extends { length: number }> =
T['length'] extends infer L extends number
? IfAny<L, number, T extends string ? Split<T, EmptyString>['length'] : L>
: never
Expand Down
58 changes: 58 additions & 0 deletions src/types/spread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @file Type Definitions - Spread
* @module tutils/types/Spread
*/

import type At from './at'
import type BuiltIn from './built-in'
import type IfIndexSignature from './if-index-signature'
import type IfNegativeInteger from './if-negative'
import type IfNumber from './if-number'
import type Indices from './indices'
import type Intersection from './intersection'
import type IsAnyOrNever from './is-any-or-never'
import type Keyof from './keyof'
import type Objectify from './objectify'
import type Stringify from './stringify'
import type Writable from './writable'

/**
* Construct a type representing the result of [spreading][1] `T`.
*
* [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax
*
* @todo examples
*
* @template T - Type to evaluate
*/
type Spread<T> = IsAnyOrNever<T> extends true
? Objectify<T>
: T extends unknown
? Objectify<T> &
(T extends string
? { [N in Indices<T> as IfNegativeInteger<N, never, N>]: At<T, N> }
: Objectify<never>) extends infer U
? Writable<{
[H in keyof U as IfIndexSignature<
U,
H,
H,
IfNumber<
H,
Stringify<H>,
T extends Readonly<BuiltIn>
? H extends Intersection<H, Keyof<BuiltIn>>
? never
: H
: H
>
>]: T extends string | readonly unknown[]
? number extends H
? At<T, H & number>
: U[H]
: U[H]
}>
: never
: never

export type { Spread as default }

0 comments on commit a6db57f

Please sign in to comment.