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

0.2.0 #27

Merged
merged 7 commits into from
Mar 8, 2017
Merged

0.2.0 #27

merged 7 commits into from
Mar 8, 2017

Conversation

gcanti
Copy link
Owner

@gcanti gcanti commented Mar 2, 2017

  • New Feature
    • add partial combinator (makes optional props possible)
    • add readonly combinator (values are not frozen in production)
    • add never type
  • Breaking Changes
    • remove maybe combinator, can be defined in userland as
      export function maybe<RT extends t.Any>(type: RT, name?: string): t.UnionType<[RT, typeof t.null], t.TypeOf<RT> | null> {
        return t.union([type, t.null], name)
      }
  • Polish
    • export pathReporterFailure function from default reporters
  • Bug Fix
    • revert pruning excess properties (see 0.2.0 #27 for context)
    • revert intersection combinator accepting only InterfaceTypes

- **New Feature**
  - add `partial` combinator
  - add `never` type
- **Breaking Changes**
  - remove `undefined` as valid value for `maybe` combinator
- **Bug Fix**
  - revert pruning excess properties
  - revert `intersection` combinator accepting only `InterfaceType`s
@gcanti gcanti added this to the 0.2 milestone Mar 2, 2017
@gyzerok
Copy link

gyzerok commented Mar 2, 2017

What is the reason for removing undefined from maybe?

It seems like maybe(string) could be easily achieved with union([undefined, null, string]) or union([null, string]) depending on your use case. Maybe (:D) it's worth considering removing maybe entirely? Since you expecting library to be low level thing it makes even more sense to not widen API by providing "sugar".

@gyzerok
Copy link

gyzerok commented Mar 2, 2017

Also can you, please, describe reasons behind revert pruning properties?

@gcanti
Copy link
Owner Author

gcanti commented Mar 2, 2017

What is the reason for removing undefined from maybe?

With respect to "optional" fields (returned by an API) there are 2 main use cases I can think of

  1. encoded with null
type Payload = {
  foo: string | null
}

Now if you extract the static type from a maybe combinator you end up with string | undefined | null instead of string | null

  1. missing key
type Payload = {
  foo?: string
}

This should be covered by the new partial combinator

const RTPayload = t.partial({ foo: t.string })
type Payload = TypeOf<typeof RTPayload> // same as { foo?: string }

However

Maybe (:D) it's worth considering removing maybe entirely?

I agree with you, we can remove the maybe combinator and let the user choose the best implementation for its own use case

Also can you, please, describe reasons behind revert pruning properties?

This is my fault, I misinterpreted the way TypeScript handles strictness, let's see some examples

const x: { a: string } = { a: 's', b: 1 } // error

is an error but only because I'm using an object literal AND the option suppressExcessPropertyErrors (in tsconfig) is set to false. In all the other cases TypeScript is not strict

// suppressExcessPropertyErrors = true
const x: { a: string } = { a: 's', b: 1 } // NO error
const y = { a: 's', b: 1 }
const x: { a: string } = y // NO error
const y = { a: 's', b: 1 }
declare function f(x: { a: string }): void
f(y) // NO error

@gcanti
Copy link
Owner Author

gcanti commented Mar 3, 2017

Added a new combinator: readonly

@gcanti gcanti force-pushed the partial branch 2 times, most recently from b6fb864 to 7d0293f Compare March 3, 2017 15:54
@gcanti
Copy link
Owner Author

gcanti commented Mar 4, 2017

Removed maybe combinator as per @gyzerok 's suggestion, can be defined in user land as

export function maybe<RT extends t.Any>(type: RT, name?: string): t.UnionType<[RT, typeof t.null], t.TypeOf<RT> | null> {
  return t.union([type, t.null], name)
}

Another interesting combinator in user land might be brand.

The problem

const payload = {
  celsius: 100,
  fahrenheit: 100
}

const Payload = t.interface({
  celsius: t.number,
  fahrenheit: t.number
})

// x can be anything
function naiveConvertFtoC(x: number): number {
  return (x - 32) / 1.8;
}

// typo: celsius instead of fahrenheit
console.log(t.validate(payload, Payload).map(x => naiveConvertFtoC(x.celsius))) // NO error :(

Solution (branded types)

export function brand<T, B extends string>(type: t.Type<T>, brand: B): t.Type<T & { readonly __brand: B }> {
  return type as any
}

const Fahrenheit = brand(t.number, 'Fahrenheit')
const Celsius = brand(t.number, 'Celsius')

type CelsiusT = t.TypeOf<typeof Celsius>
type FahrenheitT = t.TypeOf<typeof Fahrenheit>

const Payload2 = t.interface({
  celsius: Celsius,
  fahrenheit: Fahrenheit
})

// narrowed types
function convertFtoC(fahrenheit: FahrenheitT): CelsiusT {
  return (fahrenheit - 32) / 1.8 as CelsiusT;
}

console.log(t.validate(payload, Payload2).map(x => convertFtoC(x.celsius))) // error: Type '"Celsius"' is not assignable to type '"Fahrenheit"'
console.log(t.validate(payload, Payload2).map(x => convertFtoC(x.fahrenheit))) // ok

@gyzerok
Copy link

gyzerok commented Mar 6, 2017

Actually it looks like there is no much need to define maybe in the user space. Just inlining could work fine.

t.object({
  foo: t.string,
  bar: t.union([t.null, t.undefined, t.number])
})

Regarding pruning I still don't think there is much necessity in removing it. It's much easier to change behaviour to non-prune in the future since it's non-breaking change, while much harder to do otherwise from the perspective of library consumers. I'd better keep pruning for now and see if people will create issue around it. Then one can see their usecases and make decision based on them.

Also I guess it be quite useful if you can come up with the definition of what low level means from your point of view. For example partial doesn't look like a super common thing to use and could be defined as t.union([t.undefined, type]). At the same time as far as I understand the thing it will make all the properties of an object optional which is usually not the case. So I'd say that in order to be low level library should have as less API as possible, mostly things you need on every time basis (like primitive type validators).

@gcanti
Copy link
Owner Author

gcanti commented Mar 6, 2017

Just inlining could work fine

Yes, I just wanted to show how to define a custom combinator (if you define many maybe types a custom combinator is helpful in order to be DRY)

Regarding pruning I still don't think there is much necessity in removing it

The thing is that pruning was a way to align io-ts to TypeScript's behavior. But I was mistaken with respect to strictness, so is the current version that doesn't align with ts and should be fixed. Also without pruning, intersections are easier to implement and they are more general.

For example partial doesn't look like a super common thing to use and could be defined as t.union([t.undefined, type])

The extracted static types are different

const T1 = t.interface({
  foo: t.union([t.undefined, t.string])
})

type TT1 = t.TypeOf<typeof T1>
/* same as
type TT1 = {
    foo: string | undefined;
}
*/

const x1: TT1 = { foo: undefined } // ok
const x2: TT1 = {} // error

const T2 = t.partial({
  foo: t.string
})

type TT2 = t.TypeOf<typeof T2>
/* same as
type TT2 = {
    foo?: string; <= note the ? here
}
*/

const x3: TT2 = { foo: undefined } // ok
const x4: TT2 = {} // <= NO error

Without the partial combinator you can't get the ? feature of TypeScript.

This is especially useful when using react

import * as React from 'react'

const RuntimeProps = t.interface({
  foo: t.union([t.undefined, t.string])
})

type Props = t.TypeOf<typeof RuntimeProps>

class MyComponent extends React.Component<Props, void> {}

<MyComponent /> // error: Property 'foo' is missing in type...

while using partial

import * as React from 'react'

const RuntimeProps = t.partial({
  foo: t.string
})

type Props = t.TypeOf<typeof RuntimeProps>

class MyComponent extends React.Component<Props, void> {}

<MyComponent /> // NO error

@gyzerok
Copy link

gyzerok commented Mar 6, 2017

What if I have following type

interface Test {
  foo: string,
  bar?: number,
}

How one would create this with partial? Maybe I just missed something, sorry :)

@gcanti
Copy link
Owner Author

gcanti commented Mar 6, 2017

With an intersection

const T1 = t.interface({
  foo: t.string
})

const T2 = t.partial({
  bar: t.number
})

const Test = t.intersection([T1, T2])

type TestT = t.TypeOf<typeof Test>

const x1: TestT = { foo: 'a' } // ok

@gyzerok
Copy link

gyzerok commented Mar 6, 2017

Ah, ok, I see now 👍

@gcanti gcanti merged commit eb8fbd0 into master Mar 8, 2017
@gcanti gcanti deleted the partial branch March 8, 2017 06:57
@gcanti
Copy link
Owner Author

gcanti commented Mar 8, 2017

@gyzerok published a preview in the io-ts@next channel if you want to give it a spin

@gyzerok
Copy link

gyzerok commented Mar 8, 2017

@gcanti thank you! Won't be able to test it right away, but expecting to try upgrade in our project in the following days.

@gcanti
Copy link
Owner Author

gcanti commented Mar 8, 2017

@gyzerok btw, have you some source schema for the API payloads? Something like JSON Schema, swagger, etc...?
I'm working on a way to generate both static and runtime types from a generic schema so client and server can be always in sync https://github.com/gcanti/gen-io-ts

@gyzerok
Copy link

gyzerok commented Mar 8, 2017

@gcanti not really, mostly textual documentation :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants