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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

t.Type<t.TypeOf<DecoderType>> != DecoderType for branded types #576

Closed
kulla opened this issue Mar 23, 2021 · 5 comments
Closed

t.Type<t.TypeOf<DecoderType>> != DecoderType for branded types #576

kulla opened this issue Mar 23, 2021 · 5 comments

Comments

@kulla
Copy link

kulla commented Mar 23, 2021

馃悰 Bug report

Current Behavior

It might not be a bug but a misunderstanding at my part 馃檲 . I in my understanding it should be t.Type<t.TypeOf<DecoderType>> == DecoderType. It seems, that this is not true for branded types:

import * as t from "io-ts";

interface PositiveBrand {
  readonly Positive: unique symbol;
}

const Positive = t.brand(
  t.number,
  (n): n is t.Branded<number, PositiveBrand> => 0 < n,
  "Positive"
);

const foo: t.Type<t.TypeOf<typeof Positive>> = Positive;

In the last line I get the error:

test.ts:13:7 - error TS2322: Type 'BrandC<NumberC, PositiveBrand>' is not assignable to type 'Type<Branded<number, PositiveBrand>, Branded<number, PositiveBrand>, unknown>'.
  Types of property 'encode' are incompatible.
    Type 'Encode<Branded<number, PositiveBrand>, number>' is not assignable to type 'Encode<Branded<number, PositiveBrand>, Branded<number, PositiveBrand>>'.
      Type 'number' is not assignable to type 'Branded<number, PositiveBrand>'.
        Type 'number' is not assignable to type 'Brand<PositiveBrand>'.

13 const foo: t.Type<t.TypeOf<typeof Positive>> = Positive;
         ~~~


Found 1 error.

Expected behavior

I can assign the refinement Positive to t.Type<t.TypeOf<typeof Positive>>.

Reproducible example

See above

Suggested solution(s)

I have no idea 馃檲

A workaround for is to use the deprecated t.refinement() (due to the fact that the old refinements do not carry the refinement to the type level, #373 (comment) ):

const Positive = t.refinement(
  t.number,
  (n): n is t.Branded<number, PositiveBrand> => 0 < n,
  "Positive"
);

Additional context

We write a GraphQL API with Apollo ( https://github.com/serlo/api.serlo.org ) where we use decoders to validate JSON responses of different services we use. Internally we derive our types with t.TypeOf<...> in order to not have duplicate code. At one place we have a function whose argument is a decoder and where we have something like function foo<S extends SomeType>(decoder: t.Type<S>). When I wanted to use branded types I came across the above issue.

Your environment

Software Version(s)
io-ts 2.2.16
fp-ts 2.9.5
TypeScript 4.2.3

Sidenote

Thanks for the great repository which helps us a lot in our ngo work! 馃憤 鉂わ笍

@gcanti
Copy link
Owner

gcanti commented Mar 24, 2021

this compiles

const foo: t.Type<t.TypeOf<typeof Positive>, t.OutputOf<typeof Positive>> = Positive

t.Type<A> means t.Type<A, A, unknown> but in a branded codec A !== O (where O is the second type parameter of t.Type which represents the output type, number in this case)

@kulla
Copy link
Author

kulla commented Mar 27, 2021

@gcanti Thanks for your explanation!

@kulla kulla closed this as completed Mar 27, 2021
@ricardo-devis-agullo
Copy link

ricardo-devis-agullo commented Dec 7, 2021

I think this is related to this issue.

How can I type the get function so it works? O type doesn't work here, because that would give me a string[] instead of a UUID[]

import * as t from 'io-ts';
import { UUID } from 'io-ts-types/lib/UUID';

declare function get<T>(key: string, type: t.Type<T>): T;

// Works ok
const stringArray: string[] = get('mykey', t.array(t.string));

// Fails with
// Argument of type 'ArrayC<BrandC<StringC, UUIDBrand>>' is not assignable to parameter of
//             type 'Type<Branded<string, UUIDBrand>[], Branded<string, UUIDBrand>[], unknown>'.
const uuidArray: UUID[] = get('mykey', t.array(UUID));

@mlegenhausen
Copy link
Contributor

mlegenhausen commented Dec 7, 2021

There are several ways to do this.

declare function get<T>(key: string, type: t.Type<T, any>): T;
declare function get<T extends t.Mixed>(key: string, type: T): t.TypeOf<T>;

When you take a look in t.Type you will see that O defaults to A. If you want to use a different type for O you need to explicitly set it e.g. to any.

@ricardo-devis-agullo
Copy link

Yes, that's it! Thank you very much

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

No branches or pull requests

4 participants