Skip to content

Commit

Permalink
feat(types): IsOptionalKey, IfOptionalKey
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed May 24, 2023
1 parent 77e6053 commit c0ebf6f
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 33 deletions.
9 changes: 7 additions & 2 deletions __fixtures__/author.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
*/
interface Author {
/**
* Author's email address.
* Author display name.
*/
email: Lowercase<string>
display_name?: string | undefined

/**
* Author's public email address.
*/
email?: Lowercase<string>

/**
* Author's first name.
Expand Down
11 changes: 5 additions & 6 deletions src/types/__tests__/get.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,25 @@ describe('unit-d:types/Get', () => {

it('should equal T[K] with respect for dot notation', () => {
// Arrange
type AuthorEnhanced = Author & { display_name?: { value: string } }
type T = {
authors: [AuthorEnhanced]
authors: [Author]
donated_by?: { email: Lowercase<string>; name: string }
isbn: number
readers: Map<string, string[]>
title: string
}
type K1 = 'authors.0'
type K2 = 'authors.0.display_name.value'
type K2 = 'authors.0.display_name'
type K3 = 'authors.0.email'
type K4 = 'authors.-1.email'
type K5 = 'authors.-2.email'
type K6 = 'donated_by.name'

// Expect
expectTypeOf<TestSubject<T, K1>>().toEqualTypeOf<AuthorEnhanced>()
expectTypeOf<TestSubject<T, K1>>().toEqualTypeOf<Author>()
expectTypeOf<TestSubject<T, K2>>().toEqualTypeOf<string | undefined>()
expectTypeOf<TestSubject<T, K3>>().toEqualTypeOf<Lowercase<string>>()
expectTypeOf<TestSubject<T, K4>>().toEqualTypeOf<Lowercase<string>>()
expectTypeOf<TestSubject<T, K3>>().toEqualTypeOf<Author['email']>()
expectTypeOf<TestSubject<T, K4>>().toEqualTypeOf<Author['email']>()
expectTypeOf<TestSubject<T, K5>>().toBeUndefined()
expectTypeOf<TestSubject<T, K6>>().toEqualTypeOf<string | undefined>()
})
Expand Down
28 changes: 28 additions & 0 deletions src/types/__tests__/if-key-optional.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @file Type Tests - IfOptionalKey
* @module tutils/types/tests/unit-d/IfOptionalKey
*/

import type Author from '#fixtures/author.interface'
import type TestSubject from '../if-key-optional'

describe('unit-d:types/IfOptionalKey', () => {
type False = false
type True = true

it('should equal False if IsOptionalKey<T, K> extends false', () => {
// Arrange
type K = 'last_name'

// Expect
expectTypeOf<TestSubject<Author, K, True, False>>().toEqualTypeOf<False>()
})

it('should equal True if IsOptionalKey<T, K> extends true', () => {
// Arrange
type K = 'email'

// Expect
expectTypeOf<TestSubject<Author, K, True, False>>().toEqualTypeOf<True>()
})
})
29 changes: 29 additions & 0 deletions src/types/__tests__/is-key-optional.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @file Type Tests - IsOptionalKey
* @module tutils/types/tests/unit-d/IsOptionalKey
*/

import type Author from '#fixtures/author.interface'
import type TestSubject from '../is-key-optional'

describe('unit-d:types/IsOptionalKey', () => {
it('should equal false if K is not optional property of T', () => {
// Arrange
type K1 = 'first_name'
type K2 = 'last_name'

// Expect
expectTypeOf<TestSubject<Author, K1>>().toEqualTypeOf<false>()
expectTypeOf<TestSubject<Author, K2>>().toEqualTypeOf<false>()
})

it('should equal true if K is optional property of T', () => {
// Arrange
type K1 = 'display_name'
type K2 = 'email'

// Expect
expectTypeOf<TestSubject<Author, K1>>().toEqualTypeOf<true>()
expectTypeOf<TestSubject<Author, K2>>().toEqualTypeOf<true>()
})
})
24 changes: 13 additions & 11 deletions src/types/__tests__/join.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@
*/

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

describe('unit-d:types/Join', () => {
it('should equal A.join(Delimiter)', () => {
// Arrange
type A = []
type A1 = [1, 2, 3]
type A2 = [1n, 2n, 3n]
type A3 = [false, true, false]
type A4 = ['arr', ...number[]]
type A5 = [...A4, keyof Author]
type A1 = EmptyArray
type A2 = [1, 2, 3]
type A3 = [1n, 2n, 3n]
type A4 = [false, true, false]
type A5 = ['arr', ...number[]]
type A6 = [...A5, keyof Author]

// Expect
expectTypeOf<TestSubject<A>>().toEqualTypeOf<''>()
expectTypeOf<TestSubject<A1>>().toEqualTypeOf<'1.2.3'>()
expectTypeOf<TestSubject<A1>>().toEqualTypeOf<''>()
expectTypeOf<TestSubject<A2>>().toEqualTypeOf<'1.2.3'>()
expectTypeOf<TestSubject<A3>>().toEqualTypeOf<'false.true.false'>()
expectTypeOf<TestSubject<A4>>().toEqualTypeOf<`arr.${string}`>()
expectTypeOf<TestSubject<A5>>().toEqualTypeOf<
expectTypeOf<TestSubject<A3>>().toEqualTypeOf<'1.2.3'>()
expectTypeOf<TestSubject<A4>>().toEqualTypeOf<'false.true.false'>()
expectTypeOf<TestSubject<A5>>().toEqualTypeOf<`arr.${string}`>()
expectTypeOf<TestSubject<A6>>().toEqualTypeOf<
| `arr.${string}.display_name`
| `arr.${string}.email`
| `arr.${string}.first_name`
| `arr.${string}.last_name`
Expand Down
16 changes: 8 additions & 8 deletions src/types/__tests__/path.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,7 @@ describe('unit-d:types/Path', () => {
it('should equal union if T extends ObjectAny', () => {
// Arrange
interface Book {
authors: {
display_name?: { value: string }
email: Lowercase<string>
first_name: string
last_name: string
}[]
authors: Author[]
fn?: Fn & { id: number & { tag: 'book' } }
isbn: number
publisher: {
Expand Down Expand Up @@ -63,7 +58,6 @@ describe('unit-d:types/Path', () => {
| 'publisher'
| 'readers'
| 'title'
| `authors.${number}.display_name.value`
| `authors.${number}.display_name`
| `authors.${number}.email`
| `authors.${number}.first_name`
Expand Down Expand Up @@ -104,7 +98,13 @@ describe('unit-d:types/Path', () => {
type A1 = Author[] & { matcher: RegExp }
type A2 = { matcher: RegExp } & [Author]
type E1 = Numeric | 'matcher' | `${Numeric}.${keyof Author}`
type E2 = '0.email' | '0.first_name' | '0.last_name' | '0' | 'matcher'
type E2 =
| '0.display_name'
| '0.email'
| '0.first_name'
| '0.last_name'
| '0'
| 'matcher'

// Expect
expectTypeOf<TestSubject<A1>>().toEqualTypeOf<E1>()
Expand Down
27 changes: 27 additions & 0 deletions src/types/if-key-optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @file Type Definitions - IfOptionalKey
* @module tutils/types/IfOptionalKey
*/

import type IndexSignature from './index-signature'
import type IsOptionalKey from './is-key-optional'

/**
* Conditional type that resolves depending on whether or not `K` is an optional
* property of `T`.
*
* @see {@linkcode IsOptionalKey}
*
* @template T - Type to evaluate
* @template K - Key to evaluate
* @template True - Type if `K` is optional property
* @template False - Type if `K` is not optional property
*/
type IfOptionalKey<T, K extends IndexSignature, True, False> = IsOptionalKey<
T,
K
> extends true
? True
: False

export type { IfOptionalKey as default }
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type { default as IfBoolean } from './if-boolean'
export type { default as IfEqual } from './if-equal'
export type { default as IfFunction } from './if-function'
export type { default as IfJsonPrimitive } from './if-json-primitive'
export type { default as IfOptionalKey } from './if-key-optional'
export type { default as IfNever } from './if-never'
export type { default as IfNil } from './if-nil'
export type { default as IfNull } from './if-null'
Expand All @@ -54,6 +55,7 @@ export type { default as IsBoolean } from './is-boolean'
export type { default as IsEqual } from './is-equal'
export type { default as IsFunction } from './is-function'
export type { default as IsJsonPrimitive } from './is-json-primitive'
export type { default as IsOptionalKey } from './is-key-optional'
export type { default as IsNever } from './is-never'
export type { default as IsNil } from './is-nil'
export type { default as IsNull } from './is-null'
Expand Down
19 changes: 19 additions & 0 deletions src/types/is-key-optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @file Type Definitions - IsOptionalKey
* @module tutils/types/IsOptionalKey
*/

import type IndexSignature from './index-signature'
import type OptionalKeys from './keys-optional'

/**
* Returns a boolean indicating if `K` is an optional property of `T`.
*
* @template T - Type to evaluate
* @template K - Key to evaluate
*/
type IsOptionalKey<T, K extends IndexSignature> = K extends OptionalKeys<T>
? true
: false

export type { IsOptionalKey as default }
8 changes: 2 additions & 6 deletions src/types/keys-optional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@
* @module tutils/types/OptionalKeys
*/

import type ObjectPlain from './object-plain'

/**
* Extracts all optional keys from `T`.
*
* @todo support nested keys
*
* @template T - Object type to evaluate
* @template T - Type to evaluate
*/
type OptionalKeys<T extends ObjectPlain> = Exclude<
type OptionalKeys<T> = Exclude<
{ [K in keyof T]: T extends Record<K, T[K]> ? never : K }[keyof T],
undefined
>
Expand Down

0 comments on commit c0ebf6f

Please sign in to comment.