Skip to content

Commit

Permalink
feat(types): Pick, PickNative
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
  • Loading branch information
unicornware committed May 24, 2023
1 parent 41f9463 commit 42ef8cd
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 14 deletions.
7 changes: 6 additions & 1 deletion __fixtures__/book.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ interface Book {
/**
* Book publisher.
*/
publisher: Publisher
publisher?: Publisher

/**
* Book readers.
*/
readers: Map<string, string[]>

/**
* Book title.
Expand Down
5 changes: 5 additions & 0 deletions __fixtures__/publisher.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
* Object representing a book publisher.
*/
interface Publisher {
/**
* Publisher display name.
*/
display_name: { value: string }

/**
* Email address of publisher's support team.
*/
Expand Down
6 changes: 2 additions & 4 deletions src/types/__tests__/get.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type Author from '#fixtures/author.interface'
import type Book from '#fixtures/book.interface'
import type At from '../at'
import type EmptyArray from '../empty-array'
import type EmptyObject from '../empty-object'
Expand Down Expand Up @@ -55,12 +56,9 @@ describe('unit-d:types/Get', () => {

it('should equal T[K] with respect for dot notation', () => {
// Arrange
type T = {
type T = Omit<Book, 'authors'> & {
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'
Expand Down
13 changes: 5 additions & 8 deletions src/types/__tests__/path.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

import type Author from '#fixtures/author.interface'
import type Book from '#fixtures/book.interface'
import type Publisher from '#fixtures/publisher.interface'
import type EmptyArray from '../empty-array'
import type EmptyObject from '../empty-object'
import type EmptyString from '../empty-string'
Expand All @@ -27,22 +29,17 @@ describe('unit-d:types/Path', () => {

it('should equal union if T extends ObjectAny', () => {
// Arrange
interface Book {
type T = Omit<Book, 'publisher'> & {
authors: Author[]
fn?: Fn & { id: number & { tag: 'book' } }
isbn: number
publisher: {
publisher: Omit<Publisher, 'display_name'> & {
display_name?: { value: string }
email: Lowercase<string>
fn?: Fn & { id: number & { tag: 'publisher' } }
name: string
}
readers: Map<string, string[]>
title: string
}

// Expect
expectTypeOf<TestSubject<Book>>().toEqualTypeOf<
expectTypeOf<TestSubject<T>>().toEqualTypeOf<
| 'authors'
| 'fn.id.tag'
| 'fn.id'
Expand Down
17 changes: 17 additions & 0 deletions src/types/__tests__/pick-native.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @file Type Tests - PickNative
* @module tutils/types/tests/unit-d/PickNative
*/

import type Author from '#fixtures/author.interface'
import type TestSubject from '../pick-native'

describe('unit-d:types/PickNative', () => {
it('should equal typescript.Pick<T, K>', () => {
// Arrange
type K = 'email'

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

import type Author from '#fixtures/author.interface'
import type Book from '#fixtures/book.interface'
import type Publisher from '#fixtures/publisher.interface'
import type TestSubject from '../pick'
import type PickNative from '../pick-native'

describe('unit-d:types/Pick', () => {
it('should equal PickNative<T, K> if K extends keyof T', () => {
// Arrange
type T = Book
type K = 'isbn' | 'title'

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

it('should pick properties with respect for dot notation', () => {
// Arrange
type K =
| 'authors.0.display_name'
| 'authors.0.email'
| 'isbn'
| 'publisher.name'
| 'title'

// Expect
expectTypeOf<TestSubject<Book, K>>().toEqualTypeOf<{
authors: {
display_name?: Author['display_name']
email?: Exclude<Author['email'], undefined>
}[]
publisher?: PickNative<Publisher, 'name'>
isbn: Book['isbn']
title: Book['title']
}>()
})
})
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export type { default as OrLowercase } from './or-lowercase'
export type { default as OrUppercase } from './or-uppercase'
export type { default as Overwrite } from './overwrite'
export type { default as Path } from './path'
export type { default as Pick } from './pick'
export type { default as PickNative } from './pick-native'
export type { default as Predicate } from './predicate'
export type { default as Primitive } from './primitive'
export type { default as Promisable } from './promisable'
Expand Down
3 changes: 2 additions & 1 deletion src/types/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/types/Join
*/

import type EmptyArray from './empty-array'
import type Fallback from './fallback'
import type Joinable from './joinable'

Expand All @@ -15,7 +16,7 @@ import type Joinable from './joinable'
type Join<
A extends readonly Joinable[],
Delimiter extends string = '.'
> = A extends []
> = A extends EmptyArray
? ''
: A extends readonly [Joinable?]
? `${Fallback<A[0], ''>}`
Expand Down
16 changes: 16 additions & 0 deletions src/types/pick-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @file Type Definitions - PickNative
* @module tutils/types/PickNative
*/

/**
* From `T`, pick a set of properties whose keys are in the union `K`.
*
* @see https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys
*
* @template T - Type to evaluate
* @template K - Keys to select
*/
type PickNative<T, K extends keyof T> = Pick<T, K>

export type { PickNative as default }
84 changes: 84 additions & 0 deletions src/types/pick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @file Type Definitions - Pick
* @module tutils/types/Pick
*/

import type At from './at'
import type EnsureString from './ensure-string'
import type Get from './get'
import type Head from './head'
import type IfOptionalKey from './if-key-optional'
import type IfExactOptionalKey from './if-key-optional-exact'
import type IfRequiredKey from './if-key-required'
import type IfNever from './if-never'
import type IfTuple from './if-tuple'
import type IfUndefined from './if-undefined'
import type Join from './join'
import type NIL from './nil'
import type NumberString from './number-string'
import type Numeric from './numeric'
import type Simplify from './simplify'
import type Tail from './tail'

/**
* From `T`, pick a set of properties whose keys are in the union `K`.
*
* Supports dot-notation for nested paths and array indexing.
*
* @template T - Type to evaluate
* @template K - Keys to select
*/
type Pick<T, K extends NumberString> = Simplify<
{
[H in Head<`${K}`>]: Get<T, H> extends infer U
? Join<[H, NumberString]> extends infer J
? Extract<K, J> extends infer X
? IfNever<
X,
U,
Tail<X> extends infer P
? NonNullable<U> extends readonly unknown[]
? Head<P> extends infer HNext
? HNext extends Numeric | number
? At<NonNullable<U>, HNext> extends infer Item
? IfUndefined<
Item,
[Item],
Pick<
NonNullable<Item>,
EnsureString<Tail<P>>
> extends infer Next
? IfTuple<NonNullable<U>, [Next], Next[]>
: never
>
: never
: Pick<NonNullable<U>, EnsureString<P>>
: never
: Pick<NonNullable<U>, EnsureString<P>>
: never
>
: never
: never
: never
} extends infer R
? NonNullable<T> extends readonly unknown[]
? Extract<T, NIL> | R
: {
[K in keyof R as IfExactOptionalKey<T, K, K, never>]?: Exclude<
R[K],
undefined
>
} & {
[K in keyof R as IfOptionalKey<
T,
K,
IfExactOptionalKey<T, K, never, K>,
never
>]?: R[K]
} & {
[K in keyof R as IfRequiredKey<T, K, K, never>]: R[K]
}
: never
>

export type { Pick as default }

0 comments on commit 42ef8cd

Please sign in to comment.