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

A few questions when upgrading from 1.3 to 1.8 #304

Closed
MastroLindus opened this issue Mar 13, 2019 · 9 comments
Closed

A few questions when upgrading from 1.3 to 1.8 #304

MastroLindus opened this issue Mar 13, 2019 · 9 comments

Comments

@MastroLindus
Copy link

MastroLindus commented Mar 13, 2019

I have a few questions on issues I am encountering when upgrading from 1.3 to 1.8.
I already created some separate issues that turned out not to be real issues but my lack of experience with the library, so in order to bother @gcanti the least possible I'll rather write all my questions here, so that we can hopefully avoid polluting the issuetracker.

I apologize if it's not the best approach.

I am using typescript 3.4.0-dev.20190312

  1. Unions don't seem to work
    t.type({foo: t.union([t.string, t.readonlyArray(t.string)])})
    gives me the error:
    Argument of type '[StringC, ReadonlyArrayC<StringC>]' is not assignable to parameter of type '[Mixed, Mixed, Mixed[] | undefined]'.
    Same thing when I try a union with t.null for a nullable type.

2)I want to extend the type coming from a t.intersection of other 2 types. Is the correct way to do something like:
t.type({...intersectionType.types[0].props, ...intersectionType.types[1].props, extraField: t.string})
?

3)Does t.partial allow extra non-specified fields similarly to t.type? Do I have to use t.exact(t.partial({})) if I want to strip the extra fields?
In case of a type with optional parameters, does it translate to:

const myType = t.intersection([
  t.strict({allMandatoryFields}),
  t.exact(t.partial({allOptionalFields})
])

?
Is this the correct way to define such a type or are there better ways?

  1. What's the correct way to use brand with a record?
interface RecordWithKeysBrand { readonly RecordWithKeys: unique symbol; }

const RecordWithKeysType = t.brand(t.record(t.string, t.string), (d): d is t.Branded<{ [key: string]: string }, RecordWithKeysBrand> => Object.keys(d).length > 0, "RecordWithKeys");

this seems to give me the following error:

 Type '{ [x: string]: string; }' is not assignable to type 'Branded<{ [x: string]: string; }, RecordWithKeysBrand>'.
      Type '{ [x: string]: string; }' is not assignable to type 'Brand<RecordWithKeysBrand>'.
        Property '[_brand]' is missing in type '{ [x: string]: string; }

Thanks for any help

@gcanti
Copy link
Owner

gcanti commented Mar 13, 2019

Unions don't seem to work

I can't repro, neither with 3.3.3333 nor with 3.4.0-dev.20190312

It depends, if for example both intersectionType.types[0] and intersectionType.types[1] are InterfaceTypes then yes you can do that

Does t.partial allow extra non-specified fields

Yes

Do I have to use t.exact(t.partial({})) if I want to strip the extra fields

Yes

Is this the correct way to define such a type or are there better ways?

exact is able to handle intersections so you can also do

const myType = t.exact(t.intersection([
  t.type({allMandatoryFields}),
  t.partial({allOptionalFields})
]))

this seems to give me the following error:

I can't repro, neither with 3.3.3333 nor with 3.4.0-dev.20190312

@MastroLindus
Copy link
Author

MastroLindus commented Mar 14, 2019

Thank you for answer 2 and 3, it's very nice that exact can handle intersections!

For answer 1 I'll try to keep investigating to check why it is happening, I'll update the issue as soon as I can.

for answer 4, the error doesn't appear when I define the brand, but when I try to assign a function parameter to it:

function foo(bar: t.TypeOf<typeof RecordWithKeysType>) {

}

foo({asd: "gdf"});

Argument of type '{ asd: string; }' is not assignable to parameter of type 'Branded<{ [x: string]: string; }, RecordWithKeysBrand>'.
  Property '[_brand]' is missing in type '{ asd: string; }' but required in type 'Brand<RecordWithKeysBrand>'.

Even using the brand definition in the docs I still have issues:

interface PositiveBrand {
  readonly Positive: unique symbol
}

const Positive = t.brand(t.number, (n): n is t.Branded<number, PositiveBrand> => n >= 0, 'Positive')
type Positive = t.TypeOf<typeof Positive>
  
  function foo(bar: Positive) {
  
  }

foo(2);   // error, Argument of type '2' is not assignable to parameter of type 'Branded<number, PositiveBrand>'.
  Type '2' is not assignable to type 'Brand<PositiveBrand>'.ts(2345)

@gcanti
Copy link
Owner

gcanti commented Mar 14, 2019

when I try to assign a function parameter to it

@MastroLindus that's the point of branded types, you MUST pass through the validation otherwise you get a static error

declare function foo(bar: t.TypeOf<typeof RecordWithKeysType>): string

const result: Either<t.Errors, string> = RecordWithKeysType.decode({ asd: 'gdf' }).map(foo)

See also https://dev.to/gcanti/functional-design-smart-constructors-14nb

@MastroLindus
Copy link
Author

@MastroLindus that's the point of branded types, you MUST pass through the validation otherwise you get a static error

Thank you, now it's clearer.
However I am not sure how to correctly apply it to my use case.
I use io-ts in a client-server application to validate all the parameters that the client sends to the server API.
The server has some generic code that for all the API methods, first validates all the parameters with io-ts and some additional logic, and then allows the method to continue or not depending on the result.

Now if I want to send a branded parameter from the client (let's say a positive integer), I have to still process it trough validation even there, otherwise I will get a static error that won't allow me to proceed.
Since all I really care is that the parameters on the server are correct, it feels a bit of an overkill...

@gcanti
Copy link
Owner

gcanti commented Mar 14, 2019

Now if I want to send a branded parameter from the client (let's say a positive integer), I have to still process it trough validation even there, otherwise I will get a static error that won't allow me to proceed

Not sure I'm following, maybe a real world example would help.

If you type the foo function as

function foo(bar: Positive) { }

that means that you are interested in a static check, that is you want to avoid this kind of errors

foo(1) // ok
foo(-1.2) // opss...

If you are not interested in such static guarantees, why using Positive (rather than number) in the first place?

@MastroLindus
Copy link
Author

MastroLindus commented Mar 14, 2019

My setup is like this (server is nodejs-based)

The server declares the public API in an array like:

[
"apiMethod1": {args: io-tsType, ret: io-tsType},
"apiMethod2": {args: io-tsType, ret: io-tsType},
]

I use this information:
statically, on the server, to ensure that my method signatures follow what's declared
statically, on the client, to ensure that my method invocations from the client follow what's declared
(so if I ever need to change a method on the server, the compiler will enforce that I'll need to modify the declared signature in the array, obliging me to update the client as well, therefore ensuring me that they are always in sync)

Also, at runtime, the server uses that information to validate the parameters passed from the client, before the API method are allowed to run. Obviously they will only run if the validation is successful.

Now, in case I want to invoke a method on the server with a branded parameter, I am forced to running through decode also on the client, even if my system guarantees that it will be already checked at runtime on the server. I want to use the branded type to ensure that the those types are valid at runtime, but honestly I don't care so much about the static guarantee, since on the server I cannot trust that the client is legit anyway.

TL;DR; I already ensure that all the parameters go through validation at runtime, and I cannot trust the static validation anyway, so having to decode all the branded types at calling time is more of a chore than useful, however I understand the logic behind it and I can live with that
For what is worth: I agree that enforcing the static guarantee is the best approach in the average scenario

@giogonzo
Copy link
Contributor

My setup is like this (server is nodejs-based)

nice, I did the same in a few fullstack-TS projects and it worked great. The only thing is making sure your route definitions can be imported straight from the client code without any nodejs noise around it :)

TL;DR; I already ensure that all the parameters go through validation at runtime, and I cannot trust the static validation anyway, so having to decode all the branded types at calling time is more of a chore than useful, however I understand the logic behind it and I can live with that

@MastroLindus this makes me think that you're not taking the full advantage of branded types on the client. Let me explain: if you have e.g. plain numbers going around through the client code instead of Positives, it means that there must be somewhere in the client app (form validation?) where you could add the "smart constructor" step to validate the data once at the frontier, and only deal with branded Positives in the rest of the code. This, other than avoiding the useless cast at API call time, should also improve type-safety throughout the client code

@MastroLindus
Copy link
Author

@giogonzo thank you for your answer!

nice, I did the same in a few fullstack-TS projects and it worked great. The only thing is making sure your route definitions can be imported straight from the client code without any nodejs noise around it :)

yes I absolutely adore working with typescript in the full stack :)

regarding your explanation:
On paper I like the idea a lot (more strictness and type-safety, yeah!), but practically it works best if you mainly use functional patterns and immutable data, as if I am not mistaken to do any operation on the branded data (adding numbers together for positive integers, or adding/removing keys on a record, etc) you always need to go through decode, use functional patterns with Either, and re-encode, and in general spread the usage of the library everywhere in the code (or build additional wrappers for it).
Something like adding 2 numbers together suddenly becomes a lot of boilerplate code.
Even if it is probably the most type-safe solution, I still think that pragmatically the benefit is not enough in this case to justify the effort, especially because of the validation step in the server.

@MastroLindus
Copy link
Author

Anyway: this issue went on a bit too far, and you already gave many answers, so I think it's fair to close it.
I'll still check what's happening for my issue number 1, but it might be something wrong in my build-step, so I'll open another issue about it if I eventually find out that something is really wrong.

Thank for your help guys!

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

3 participants