Skip to content

Commit

Permalink
feat(types): HasKey, IfKey
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 19, 2023
1 parent 7e8097d commit b65d7ed
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 0 deletions.
218 changes: 218 additions & 0 deletions src/types/__tests__/has-key.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* @file Type Tests - HasKey
* @module tutils/types/tests/unit-d/HasKey
*/

import type Person from '#fixtures/interfaces/person'
import type Vehicle from '#fixtures/types/vehicle'
import type Fn from '../fn'
import type TestSubject from '../has-key'
import type Indices from '../indices'
import type Nullable from '../nullable'
import type { tag as opaque } from '../opaque'
import type PropertyKey from '../property-key'
import type Stringify from '../stringify'

describe('unit-d:types/HasKey', () => {
it('should equal false if K is never', () => {
expectTypeOf<TestSubject<Vehicle, never>>().toEqualTypeOf<false>()
})

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

describe('IsAny<T> extends true', () => {
type T = any

describe('K extends number', () => {
it('should equal false if number does not extend K', () => {
expectTypeOf<TestSubject<T, 1>>().toEqualTypeOf<false>()
})

it('should equal true if number extends K', () => {
expectTypeOf<TestSubject<T, number>>().toEqualTypeOf<true>()
})
})

describe('K extends string', () => {
it('should equal false if string does not extend K', () => {
expectTypeOf<TestSubject<T, keyof Vehicle>>().toEqualTypeOf<false>()
})

it('should equal true if string extends K', () => {
expectTypeOf<TestSubject<T, string>>().toEqualTypeOf<true>()
})
})

describe('K extends symbol', () => {
it('should equal false if symbol does not extend K', () => {
expectTypeOf<TestSubject<T, typeof opaque>>().toEqualTypeOf<false>()
})

it('should equal true if symbol extends K', () => {
expectTypeOf<TestSubject<T, symbol>>().toEqualTypeOf<true>()
})
})
})

describe('T extends ObjectCurly', () => {
type T = Vehicle

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, 'vrm'>>().toEqualTypeOf<false>()
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})

describe('T extends Primitive', () => {
describe('T extends NIL', () => {
it('should equal false if keyof T is never', () => {
expectTypeOf<TestSubject<null, 'make'>>().toEqualTypeOf<false>()
expectTypeOf<TestSubject<undefined, 'vin'>>().toEqualTypeOf<false>()
})
})

describe('T extends bigint', () => {
type T = 0n

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})

describe('T extends boolean', () => {
type T = false

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})

describe('T extends number', () => {
type T = 0

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})

describe('T extends string', () => {
describe('IsLiteral<T> extends true', () => {
type T = 'xyz'

it('should equal false if K and keyof T do not intersect', () => {
// Arrange
type K = Stringify<Indices<T>> | '-1' | '-2' | '-3'

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

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})

it('should equal true if K extends Indices<T>', () => {
expectTypeOf<TestSubject<T, Indices<T>>>().toEqualTypeOf<true>()
})
})

describe('number extends Indices<T>', () => {
type T = string

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, string | symbol>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})
})

describe('T extends symbol', () => {
type T = typeof opaque

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})
})

describe('T extends Readonly<Fn>', () => {
type T = Readonly<Fn>

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, PropertyKey>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})

describe('T extends readonly unknown[]', () => {
describe('IsTuple<T> extends true', () => {
type T = readonly [Vehicle, Nullable<Vehicle>, Vehicle?]

it('should equal false if K and keyof T do not intersect', () => {
// Arrange
type K = '-1' | '-2' | '-3'

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

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})

it('should equal true if K extends Indices<T>', () => {
expectTypeOf<TestSubject<T, Indices<T>>>().toEqualTypeOf<true>()
})
})

describe('number extends Indices<T>', () => {
type T = Vehicle[]

it('should equal false if K and keyof T do not intersect', () => {
expectTypeOf<TestSubject<T, string | symbol>>().toEqualTypeOf<false>()
})

it('should equal true if K and keyof T intersect', () => {
expectTypeOf<TestSubject<T, keyof T>>().toEqualTypeOf<true>()
})
})
})

describe('unions', () => {
it('should distribute over unions', () => {
// Arrange
type T = Person | Vehicle

// Expect
expectTypeOf<TestSubject<T, 'name' | 'vin'>>().toEqualTypeOf<boolean>()
})
})
})
36 changes: 36 additions & 0 deletions src/types/__tests__/if-key.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @file Type Tests - IfKey
* @module tutils/types/tests/unit-d/IfKey
*/

import type Vehicle from '#fixtures/types/vehicle'
import type TestSubject from '../if-key'
import type Nilable from '../nilable'

describe('unit-d:types/IfKey', () => {
type F = 0
type T = 1

it('should equal F if HasKey<U, K> extends false', () => {
expectTypeOf<TestSubject<unknown, 'vin', T, F>>().toEqualTypeOf<F>()
})

it('should equal F if U is never', () => {
expectTypeOf<TestSubject<never, 'vrm', T, F>>().toEqualTypeOf<F>()
})

it('should equal T if HasKey<U, K> extends true', () => {
expectTypeOf<TestSubject<Vehicle, 'vin', T, F>>().toEqualTypeOf<T>()
})

describe('unions', () => {
it('should distribute over unions', () => {
// Arrange
type U = Nilable<Vehicle>
type K = keyof Vehicle

// Expect
expectTypeOf<TestSubject<U, K, T, F>>().toEqualTypeOf<F | T>()
})
})
})
54 changes: 54 additions & 0 deletions src/types/has-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @file Type Definitions - HasKey
* @module tutils/types/HasKey
*/

import type IfNegativeInteger from './if-integer-negative'
import type IfNever from './if-never'
import type Indices from './indices'
import type Intersection from './intersection'
import type Remap from './remap'
import type Stringify from './stringify'

/**
* Returns a boolean indicating if `K` and `keyof T` intersect.
*
* **Note**: If `K` and `keyof T` intersect, `K` can be used to index `T`.
*
* @todo examples
*
* @template T - Type to evaluate
* @template K - Keys to evaluate
*/
type HasKey<T, K> = IfNever<
T,
false,
IfNever<
K,
false,
T extends unknown
? K extends keyof T
? IfNever<
keyof {
[H in keyof Remap<T> as Intersection<
T extends readonly unknown[]
? Indices<T> extends infer I extends number
? number extends I
? H
: H extends I
? H | IfNegativeInteger<H, never, Stringify<H>>
: H
: never
: H,
K
>]: H
},
false,
true
>
: false
: never
>
>

export type { HasKey as default }
35 changes: 35 additions & 0 deletions src/types/if-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @file Type Definitions - IfKey
* @module tutils/types/IfKey
*/

import type HasKey from './has-key'
import type IfNever from './if-never'

/**
* Returns a type that indicates if `K` and `keyof T` intersect.
*
* **Note**: If `K` and `keyof T` intersect, `K` can be used to index `T`.
*
* @see {@linkcode HasKey}
*
* @todo examples
*
* @template U - Type to evaluate
* @template K - Keys to evaluate
* @template T - Type if `K` and `keyof T` intersect
* @template F - Type if `K` and `keyof T` do not intersect
*/
type IfKey<U, K, T, F> = IfNever<
U,
F,
U extends unknown
? K extends unknown
? HasKey<U, K> extends true
? T
: F
: F
: F
>

export type { IfKey as default }
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type { default as Flat } from './flat'
export type { default as Float } from './float'
export type { default as Fn } from './fn'
export type { default as Get } from './get'
export type { default as HasKey } from './has-key'
export type { default as HasKeys } from './has-keys'
export type { default as Head } from './head'
export type { default as IfAny } from './if-any'
Expand All @@ -46,6 +47,7 @@ export type { default as IfIndexSignature } from './if-index-signature'
export type { default as IfInteger } from './if-integer'
export type { default as IfNegativeInteger } from './if-integer-negative'
export type { default as IfJsonPrimitive } from './if-json-primitive'
export type { default as IfKey } from './if-key'
export type { default as IfOptionalKey } from './if-key-optional'
export type { default as IfExactOptionalKey } from './if-key-optional-exact'
export type { default as IfReadonlyKey } from './if-key-readonly'
Expand Down

0 comments on commit b65d7ed

Please sign in to comment.