Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] partial partial #266

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions dtslint/ts3.0/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,70 @@ const toNumber = (n: t.Int): number => n

// $ExpectError
const intToInt2 = (int: t.Int): Int2 => int

//
// partialPartial
//

const Human = t.partialPartial({
name: t.string,
age: t.optional(t.number),
})
type Human = t.TypeOf<typeof Human>

const bob: Human = { name: 'Bob' }
const bob30: Human = { name: 'Bob', age: 30 }
// $ExpectError
const bobThirty: Human = { name: 'Bob', age: 'thirty' }
// $ExpectError
const nobody: Human = {}

/**
* if a generic type is passed into a method which uses partialPartial, it could either
* be optional or not, so if specifying the return type explicitly, we have to assume
* it could be undefined on the `TypeOf` type.
*/
export const partialPartialAbstract = <Type extends t.Type<A, O>, A = any, O = any>(
type: Type
): t.Validation<{ t: 'v'; v?: t.TypeOf<Type> }> => {
const T = t.partialPartial({
t: t.literal('v'),
v: type
})
const res = T.validate(1 as any, [])
res.map(x => {
x.t // $ExpectType "v"
// `x.v` could be undefined; we don't know whether `type` is optional here:
x.v // $ExpectType Type["_A"] | undefined
})
return res
}

/**
* to avoid the problem above, rely on type inference, which will keep track of the
* generic type to figure out if `v` should be optional.
*/
export const partialPartialAbstractInferred = <Type extends t.Type<A, O>, A = any, O = any>(
type: Type
) => {
const T = t.partialPartial({
t: t.literal('v'),
v: type
})
const res = T.validate(1 as any, [])
res.map(x => {
x.t // $ExpectType "v"
// `x.v` could be undefined; we don't know whether `type` is optional here:
x.v // $ExpectType Type["_A"] | undefined
})
return res
}

const inferredWithOptional = partialPartialAbstractInferred(t.optional(t.string))
const vShouldBeOptional = inferredWithOptional.getOrElse(0 as any)
const vNotSuppliedOk: typeof vShouldBeOptional = { t: 'v' }

const inferredWithoutOptional = partialPartialAbstractInferred(t.string)
const vShouldBeRequired = inferredWithoutOptional.getOrElse(0 as any)
// $ExpectError
const vNotSuppliedFailure: typeof vShouldBeRequired = { t: 'v' }
43 changes: 43 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,49 @@ export interface Props {
export interface TypeC<P extends Props>
extends InterfaceType<P, { [K in keyof P]: TypeOf<P[K]> }, { [K in keyof P]: OutputOf<P[K]> }, unknown> {}

interface Optional {
optional: true
}

export const optional = <T>(rt: Type<T>, name?: string) => {
const unionType = union([rt, nullType, undefinedType], name || rt.name + '?')
return Object.assign(unionType, { optional: true } as Optional)
}

type OptionalPropsTypes<P extends Props> = { [K in keyof P]?: P[K]['_A'] }
type OptionalPropsOutputs<P extends Props> = { [K in keyof P]?: P[K]['_O'] }
type RequiredPropsKeys<P extends Props> = { [K in keyof P]: P[K] extends Optional ? never : K }[keyof P]
type RequiredPropsTypes<P extends Props> = { [K in RequiredPropsKeys<P>]: P[K]['_A'] }
type RequiredPropsOutputs<P extends Props> = { [K in RequiredPropsKeys<P>]: P[K]['_O'] }

export const partialPartial = <P extends Props>(
props: P,
name?: string
): Type<OptionalPropsTypes<P> & RequiredPropsTypes<P>, OptionalPropsOutputs<P> & RequiredPropsOutputs<P>, mixed> => {
let someOptional = false
let someRequired = false
const optionalProps: Props = {}
const requiredProps: Props = {}
for (const key of Object.keys(props)) {
const val: any = props[key]
if (val.optional) {
someOptional = true
optionalProps[key] = val
} else {
someRequired = true
requiredProps[key] = val
}
}
const computedName = name || getInterfaceTypeName(props)
if (someOptional && someRequired) {
return intersection([type(requiredProps), partial(optionalProps)], computedName) as any
} else if (someOptional) {
return partial(props, computedName) as any
} else {
return type(props, computedName) as any
}
}

const getInterfaceTypeName = (props: Props): string => {
return `{ ${getNameFromProps(props)} }`
}
Expand Down
28 changes: 28 additions & 0 deletions test/partialPartial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as t from '../src/index'
import { Right, Left } from 'fp-ts/lib/Either'

describe('partialPartial', () => {
it('handles some optional props', () => {
const Person = t.partialPartial({ name: t.string, age: t.optional(t.number) })
expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left)
expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right)
expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Right)
expect(Person.decode({})).toBeInstanceOf(Left)
})

it('handles all required props', () => {
const Person = t.partialPartial({ name: t.string, age: t.number })
expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left)
expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right)
expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Left)
expect(Person.decode({})).toBeInstanceOf(Left)
})

it('handles all optional props', () => {
const Person = t.partialPartial({ name: t.optional(t.string), age: t.optional(t.number) })
expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left)
expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right)
expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Right)
expect(Person.decode({})).toBeInstanceOf(Right)
})
})