Skip to content

Commit

Permalink
fix: partial props (#5)
Browse files Browse the repository at this point in the history
* fix: Assembler props allow explicit undefined
* fix: Partial and async assembler return types
  • Loading branch information
timkinnane authored Jul 13, 2021
1 parent 34b2bdd commit be0df3c
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 29 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Assembler<Props, 'name' | 'message'>
// ➥ (props: Props) => { message: string, name: string }

PartialAssembler<Props, 'message'>
// ➥ (props: Props) => { message?: string }
// ➥ (props: Props) => { message?: string } | undefined

VoidAssembler<Props>
// ➥ (props: Props) => void
Expand Down
44 changes: 26 additions & 18 deletions src/assemble.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { assemble, assembleSync } from './assemble'
import { Assembler, AsyncAssembler, VoidAssembler } from './types'
import { Assembler, AsyncAssembler, PartialAssembler, VoidAssembler } from './types'

interface TestProps {
one?: boolean
two?: boolean
three?: boolean
three?: boolean,
maybe?: boolean | undefined
}

describe('PipeType', () => {
Expand All @@ -15,21 +16,29 @@ describe('PipeType', () => {
expect(testFn).toBeCalledTimes(3)
})
it('Combines sync and async assemblers returns', async () => {
const testAsync: AsyncAssembler<TestProps, 'one'> = async () =>
({ one: await Promise.resolve(true) })
const testSync: Assembler<TestProps, 'two'> = () =>
({ two: true })
await expect(
assemble(testAsync, testSync)({ })
).resolves.toEqual({ one: true, two: true })
const testAsync: AsyncAssembler<TestProps, 'one'> = async () => ({
one: await Promise.resolve(true)
})
const testSync: Assembler<TestProps, 'two'> = () => ({
two: true
})
await expect(assemble(testAsync, testSync)({}))
.resolves.toEqual({ one: true, two: true })
})
it('Processes partially assembled or undefined props', async () => {
const getMaybe: Assembler<TestProps, 'maybe'> = () => ({
maybe: undefined
})
const getTwoIfOne: PartialAssembler<TestProps, 'two'> = ({ one }) =>
one ? { two: true } : undefined
await expect(assemble(getMaybe, getTwoIfOne)({}))
.resolves.toEqual({ maybe: undefined })
})
it('Accepts anonymous functions', async () => {
await expect(
assemble(
() => ({ one: true }),
({ one }) => ({ two: !one })
)({})
).resolves.toEqual({ one: true, two: false })
await expect(assemble(
() => ({ one: true }),
({ one }) => ({ two: !one })
)({})).resolves.toEqual({ one: true, two: false })
})
})
describe('assembleSync', () => {
Expand All @@ -38,9 +47,8 @@ describe('PipeType', () => {
({ one: true })
const testSync: Assembler<TestProps, 'two'> = () =>
({ two: true })
expect(
assembleSync(testAsync, testSync)({})
).toEqual({ one: true, two: true })
expect(assembleSync(testAsync, testSync)({}))
.toEqual({ one: true, two: true })
})
})
})
2 changes: 1 addition & 1 deletion src/examples/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ mixedAssembly({})

// @todo Adding input props that aren't in function prop types should be a type error
mixedAssembly({ b: true, shouldError: true })
// Return Props => { a: boolean, b: true, foo: boolean, one: boolean, two?: boolean }
// Return Props => { a?: boolean, b: true, foo: boolean, one: boolean, two?: boolean }
// ☝️ Because `b` is given, its prop type is narrowed to a literal on the return type.
15 changes: 8 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
MapUnwrapPromises,
MergeUnion,
FilterObjects,
UnknownFunction
UnknownFunction,
NonPartial
} from './util'

/**
Expand All @@ -16,19 +17,19 @@ import {
* }
*/
export interface Assembler<Props, Key extends keyof Props> {
(props: Props): Required<Pick<Props, Key>>
(props: Props): NonPartial<Pick<Props, Key>>
}

/**
* Function that operates on assembly props, optionally returning subset of props.
* @example
* interface Props { a?: boolean, b?: boolean }
* const maybeAssignB: PartialAssembler<Props, 'b' | undefined> = ({ a }) => {
* const maybeAssignB: PartialAssembler<Props, 'b'> = ({ a }) => {
* if (a) return { b: true }
* }
*/
export interface PartialAssembler<Props, Key extends keyof Props> {
(props: Props): Partial<Pick<Props, Key>>
(props: Props): Partial<Pick<Props, Key>> | undefined
}

/**
Expand Down Expand Up @@ -62,23 +63,23 @@ export type SyncAssemblers<Props> = Array<
* @see Assembler — with promise wrapped return.
*/
export interface AsyncAssembler<Props, Key extends keyof Props> {
(props: Props): Promise<Required<Pick<Props, Key>>>
(props: Props): Promise<NonPartial<Pick<Props, Key>>>
}

/**
* Function that operates on assembly props, optionally resolving to subset of props.
* @see PartialAssembler
*/
export interface AsyncPartialAssembler<Props, Key extends keyof Props> {
(props: Props): Partial<Pick<Props, Key>>
(props: Props): Promise<Partial<Pick<Props, Key>> | undefined>
}

/**
* Async function that operates on assembly props, resolves to void.
* @see VoidAssembler
*/
export interface AsyncVoidAssembler<Props> {
(props: Props): void
(props: Props): Promise<void>
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export type MapUnwrapPromises<T> = {
export type FilterObjects<T> = {
[K in keyof T]: T[K] extends UnknownObject ? T[K] : never
}

/** Make all props not optional without removing undefined from value types. */
export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
12 changes: 12 additions & 0 deletions test/dts-jest/NonPartial.test.snap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import test from '../testType'
import { NonPartial } from '../../src/util'

type TestProps = {
foo?: string | undefined
}

// @dts-jest:group NonPartial
{
// @dts-jest:snap 💁 { foo: string | undefined } -> NonPartial<TestProps>
test<NonPartial<TestProps>>()
}
12 changes: 12 additions & 0 deletions test/dts-jest/NonPartial.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import test from '../testType'
import { NonPartial } from '../../src/util'

type TestProps = {
foo?: string | undefined
}

// @dts-jest:group NonPartial
{
// @dts-jest:snap 💁 { foo: string | undefined }
test<NonPartial<TestProps>>()
}
1 change: 0 additions & 1 deletion test/dts-jest/Props.test.snap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import test from '../testType'
import { AssembledProps, AssemblyProps } from '../../src/types'
import { UnknownFunction } from '../../src/util'

type TestFunctions = [
() => { a: true },
Expand Down
1 change: 0 additions & 1 deletion test/dts-jest/Props.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import test from '../testType'
import { AssembledProps, AssemblyProps } from '../../src/types'
import { UnknownFunction } from '../../src/util'

type TestFunctions = [
() => { a: true },
Expand Down
3 changes: 3 additions & 0 deletions test/dts-jest/__snapshots__/NonPartial.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NonPartial 💁 { foo: string | undefined } (type) should match snapshot 1`] = `"NonPartial<TestProps>"`;

0 comments on commit be0df3c

Please sign in to comment.