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

remove type param from axios such that responses and requests are all now mixed #2464

Conversation

LoganBarnett
Copy link
Contributor

@LoganBarnett LoganBarnett commented Jul 4, 2018

eslint was complaining so I had to bypass the check - there's something it doesn't like about the [[call]] syntax, even though this is now preferred to $call. This might be related to #2405. I did not change the [[call]] syntax, so I'm not sure this worked prior.

This PR removes the polymorphic types for axios. This both decouples the request and response payloads (a possible regression of #1975), and improves soundness by moving the validation/deserialization to the consumer (or a blind any cast, which is really what was going on before).

Addresses #2461.

@AndrewSouthpaw AndrewSouthpaw self-requested a review July 13, 2018 14:41
@AndrewSouthpaw
Copy link
Contributor

It seems to me that setting the return types as mixed will require frustrating any-casting to actually interact with the return time. If you do something like

axios.get('/foo').then(res => { console.log(res.foo) })

does that throw an error? Can you write a test and make sure that's possible?

@gantoine gantoine added enhancement An addition to an existing component question libdef Related to a library definition labels Jul 16, 2018
Copy link
Contributor

@AndrewSouthpaw AndrewSouthpaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting for response to comments

@LoganBarnett
Copy link
Contributor Author

@AndrewSouthpaw being pedantic here: The response itself does not have foo, so the code in your example would not and should not type check.

Perhaps you meant:

axios.get('/foo').then(res => { console.log(res.data.foo) })

In this case it will fail to type check as well (rightfully so). You aren't strictly required to cast to any here. You could check to see if foo was present and what type it was before attempting to use it. These examples are doing the exact same thing in Flow's eyes:

type Foo = { foo: string }
(res: $AxiosXHR<Foo>) => console.log(res.data.foo.toUpperCase()
type Foo = { foo: string }
(res) => console.log(((res.data: any): Foo).foo.toUpperCase()

In both cases, you're saying "Flow just trust me on this one" and we all know what kind of lie that is to Flow and ourselves ;) If we treat data as mixed, it puts the burden on us to ensure that whatever data is, we have to do our due diligence first before we do anything meaningful with it. Granted that for complex data types, this can be a lot of work. To address that, there are some type safe validation libraries out there such as validated and (shameless plug) flow-degen. There are others out there as well.

@AndrewSouthpaw
Copy link
Contributor

AndrewSouthpaw commented Jul 24, 2018

RE: foo on res, yes, you're quite right, I meant res.data.foo.

Thanks for expanding on your thoughts! I hear them, and they make sense.

For me, at most, as a consumer of axios I'd want to do something like

type Foo = { foo: string }
(res: $AxiosXHR<Foo>) => console.log(res.data.foo.toUpperCase()

If I had to do

(res: $AxiosXHR<Foo>) => {
  if (res.data.foo) {
    ...
  }
}

or anything else like it using a validation library, I would get very, very grumpy. Yes, it's not technically type safe, and also I'm okay with that; it's something I like about Javascript, that's it's not actually a typed language and sometimes I can choose to not worry about it without having to validate everything. If I'm actually concerned about it I can pass it through the validation, but generally I'd rather not worry about it.

I think that's a reflection of how I like to code and what purpose I want Flow to serve for me. Everyone is different. So, my vote is to not merge this PR. I'm also happy to be outvoted and just keep my own older version locally. @gantoine @villesau @adityavohra7. Feel free to CC other people e.g. maintainers of this libdef if you want.

@LoganBarnett LoganBarnett force-pushed the 2461-axios-use-mixed-payloads branch from 25cd0eb to b4503d4 Compare July 24, 2018 14:12
@LoganBarnett
Copy link
Contributor Author

@AndrewSouthpaw perhaps the problem here is that we don't have a great way to do a forceful cast? At risk of being obvious, I'm in the camp of wanting more "soundness" which necessitates more strictness. That said, I can continue to use $AxiosXHR<mixed> for my response types, so I needn't die on this hill ;) But I'm still curious what others think. I feel like we've had some fair discussion over how far we want to go, but I'm not aware that there's any stated goals here, or some bar we want to meet.

As an aside, the CI fails with apt-get install failed which would be impressive if I caused that :) Is the CI environment known to not be super reliable?

@AndrewSouthpaw
Copy link
Contributor

Man, what I'd do for a forceful recast that wasn't ((foo: any): MyType). Ugh. It just looks so awful, and the lack of a forceful recast (to me) makes stuff like mixed pretty much useless.

Yeah curious to hear input from others.

RE: aside: The CI tests are ... kinda crap. Restarted.

@villesau
Copy link
Member

villesau commented Aug 1, 2018

I'm with @LoganBarnett here. Soundness should be priority, and that's the reason we even use tools like Flow :)

Although, I totally understand @AndrewSouthpaw s point, and checks would probably make annoy me too. But I have news for you: Flow have supported optional chaining now for a while which makes this kind of checks a lot easier :)

So, I would go with mixed. This is my semi-objective opinion as I'm not using Axios so I don't feel the pain either :D

@AndrewSouthpaw
Copy link
Contributor

@villesau A couple thoughts about optional chaining... it's only stage 1 right now, so offering it as a solution doesn't seem fair to the community. The bigger issue in my opinion is that it only solves part of the problem. My example wasn't complete. Here's what I'm actually afraid of needing to do:

type Foo = { a: string, b: number, c: SomeOtherComplexType }
axios.get('/foo').then((res: $AxiosXHR<mixed>) => {
  if (typeof res.data.a === 'string' && typeof res.data.b === 'number' /* && argh wtf c */) {
    ...
  }
})

Not only is that annoying to write out (or maintain a bunch of type-checking helpers), basically my fear is that we'd be forcing developers to perform a lot of runtime checks in order to provide type safety. It's potentially (probably?) totally unfounded, but I'm having concerns about performance impact on slow phones, and also the crummy dev experience of just having to actually perform type checking on every blasted AJAX request in my codebase.

I'm also definitely willing to be overruled here in the face of a majority vote. I'd love to get some other opinions to weigh in, and I can shamefully override my axios libdef and live dangerously if people decide my opinion is not the winning one. 😉

@villesau
Copy link
Member

villesau commented Aug 8, 2018

it's only stage 1 right now, so offering it as a solution doesn't seem fair to the community

Well.. JSX is not part of Javascript standard and neither is Flow it self so you will always need a transpiler for your Flow code. And flow clearly seems to go optional chaining path as they have implemented the support for it. At least they have strong belief in optional chaining, I don't think that kind of features are implemented just for fun. There are only very few experimental Javascript features Flow supports optional chaining being one of them.

But your example indeed looks very annoying. Didn't think of more complex objects in previous message. Do we have any good examples of other similar cases?

@AndrewSouthpaw
Copy link
Contributor

AndrewSouthpaw commented Aug 15, 2018

Well.. JSX is not part of Javascript standard ... one of them.

Fair point. I think we draw the line in different places, and I do see what you're saying.

As far as complex cases, I could point to any number of them within our codebase. We have plenty server calls and they often return complex objects of different types.

Did you want actual examples, or something else?

@villesau
Copy link
Member

I was thinking about examples of similar libraries and their libdefs :)

@LoganBarnett
Copy link
Contributor Author

I have an example with a relatively flat structure:

export type CellFields = {
  frontImage: string,
  rightImage: string,
  bottomImage: string,
  topImage: string,
  backImage: string,
  leftImage: string,
}

export type Cell = {
  fieldType: 'cell',
  _id: DbId,
  fields: CellFields,
  name: string,
  definitionId: DbId,
  fields: {
    frontImage: string,
    topImage: string,
    rightImage: string,
    bottomImage: string,
    backImage: string,
    leftImage: string,
  },
}

This is one of my simpler ones. The validation code for it is downright monstrous at ~240 lines:

export default (json: mixed): Cell | Error => {
  if (json === null) {
    return new Error('Could not deserialize json because the value is null.')
  } else if (typeof json == 'undefined') {
    return new Error(
      'Could not deserialize json because the value is undefined.',
    )
  } else if (json instanceof Error || typeof json != 'object') {
    return new Error('Could not deserialize object "' + String(json) + '"')
  } else {
    const name = deField('name', deString, json.name)
    if (name instanceof Error) {
      const error: Error = name
      return new Error('Could not deserialize field "name": ' + error.message)
    } else {
      const fieldType = deField(
        'fieldType',
        (x: mixed) => {
          if (typeof x != 'string') {
            return new Error(
              'Could not deserialize "' + String(x) + '" into a string.',
            )
          } else if (x === 'cell') {
            return x
          } else {
            return new Error(
              'Could not deserialize "' +
                String(x) +
                '" into a string with the value "cell".',
            )
          }
        },
        json.fieldType,
      )
      if (fieldType instanceof Error) {
        const error: Error = fieldType
        return new Error(
          'Could not deserialize field "fieldType": ' + error.message,
        )
      } else {
        const _id = deField(
          '_id',
          (v: mixed | Error) => {
            const either = deString(v)
            if (either instanceof Error) {
              return either
            } else if (typeof either == 'string') {
              if (either.match(/^[a-f0-9]+$/)) {
                return (either: DbId)
              } else {
                return new Error(
                  'Could not deserialize "' +
                    String(v) +
                    '" into a valid database ID.',
                )
              }
            } else {
              return new Error(
                'Could not deserialize "' +
                  String(v) +
                  '" into a valid database ID.',
              )
            }
          },
          json._id,
        )
        if (_id instanceof Error) {
          const error: Error = _id
          return new Error(
            'Could not deserialize field "_id": ' + error.message,
          )
        } else {
          const definitionId = deField(
            'definitionId',
            (v: mixed | Error) => {
              const either = deString(v)
              if (either instanceof Error) {
                return either
              } else if (typeof either == 'string') {
                if (either.match(/^[a-f0-9]+$/)) {
                  return (either: DbId)
                } else {
                  return new Error(
                    'Could not deserialize "' +
                      String(v) +
                      '" into a valid database ID.',
                  )
                }
              } else {
                return new Error(
                  'Could not deserialize "' +
                    String(v) +
                    '" into a valid database ID.',
                )
              }
            },
            json.definitionId,
          )
          if (definitionId instanceof Error) {
            const error: Error = definitionId
            return new Error(
              'Could not deserialize field "definitionId": ' + error.message,
            )
          } else {
            const fields = deField(
              'fields',
              (json: mixed): CellFields | Error => {
                if (json === null) {
                  return new Error(
                    'Could not deserialize json because the value is null.',
                  )
                } else if (typeof json == 'undefined') {
                  return new Error(
                    'Could not deserialize json because the value is undefined.',
                  )
                } else if (json instanceof Error || typeof json != 'object') {
                  return new Error(
                    'Could not deserialize object "' + String(json) + '"',
                  )
                } else {
                  const frontImage = deField(
                    'frontImage',
                    deString,
                    json.frontImage,
                  )
                  if (frontImage instanceof Error) {
                    const error: Error = frontImage
                    return new Error(
                      'Could not deserialize field "frontImage": ' +
                        error.message,
                    )
                  } else {
                    const topImage = deField(
                      'topImage',
                      deString,
                      json.topImage,
                    )
                    if (topImage instanceof Error) {
                      const error: Error = topImage
                      return new Error(
                        'Could not deserialize field "topImage": ' +
                          error.message,
                      )
                    } else {
                      const rightImage = deField(
                        'rightImage',
                        deString,
                        json.rightImage,
                      )
                      if (rightImage instanceof Error) {
                        const error: Error = rightImage
                        return new Error(
                          'Could not deserialize field "rightImage": ' +
                            error.message,
                        )
                      } else {
                        const bottomImage = deField(
                          'bottomImage',
                          deString,
                          json.bottomImage,
                        )
                        if (bottomImage instanceof Error) {
                          const error: Error = bottomImage
                          return new Error(
                            'Could not deserialize field "bottomImage": ' +
                              error.message,
                          )
                        } else {
                          const backImage = deField(
                            'backImage',
                            deString,
                            json.backImage,
                          )
                          if (backImage instanceof Error) {
                            const error: Error = backImage
                            return new Error(
                              'Could not deserialize field "backImage": ' +
                                error.message,
                            )
                          } else {
                            const leftImage = deField(
                              'leftImage',
                              deString,
                              json.leftImage,
                            )
                            if (leftImage instanceof Error) {
                              const error: Error = leftImage
                              return new Error(
                                'Could not deserialize field "leftImage": ' +
                                  error.message,
                              )
                            } else {
                              const result: CellFields = {
                                frontImage,
                                topImage,
                                rightImage,
                                bottomImage,
                                backImage,
                                leftImage,
                              }
                              return result
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              json.fields,
            )
            if (fields instanceof Error) {
              const error: Error = fields
              return new Error(
                'Could not deserialize field "fields": ' + error.message,
              )
            } else {
              const result: Cell = {
                name,
                fieldType,
                _id,
                definitionId,
                fields,
              }
              return result
            }
          }
        }
      }
    }
  }
}

0.0

I have another one that's closer to 1900 similar lines. I'll spare your scroll wheel for today though ;)

This code is all generated for me via a lib. The definition is a lot more lightweight:

export default () => degenObject(cellType, [
  degenField('name', degenString()),
  degenField('fieldType', degenValue('string', 'cell')),
  degenField('_id', degenDbId()),
  degenField('definitionId', degenDbId()),
  degenField('fields', degenObject(cellFieldsType, [
    degenField('frontImage', degenFilePath()),
    degenField('topImage', degenFilePath()),
    degenField('rightImage', degenFilePath()),
    degenField('bottomImage', degenFilePath()),
    degenField('backImage', degenFilePath()),
    degenField('leftImage', degenFilePath()),
  ])),
])

I've done a fair amount of work with validated as well. The definition work is similar, has a lower runtime size impact, but has to make some soundness sacrifices to support the generic runtime behavior it has.

I've been using both of these (my work decided they liked validated better, but I'd already done my code-gen stuff for prior personal work), and it's helped catch a number of issues even when we control both the client and server.

While it's easy for me to lean on the soundness argument here, I want to be mindful of biases introduced by my ecosystem. My Flow based projects enforce 100% type coverage, and don't have any unit tests (generally I make database skins for a living, so unit tests tend to only cover what Flow covers already). That means my types need to be solid though since I'm leaning hard on Flow. I'm okay with bringing in a validation/deserialization tool, but I understand that this change to the axios libdef will essentially require such a tool for any non-trivial payload. Or we make a less than pretty any cast.

FWIW there's no rush here on this PR. Right now we're getting away with a local override :) I think we've made some local adjustments to it as well, which I will try to propagate back in here today.

@AndrewSouthpaw
Copy link
Contributor

I have another one that's closer to 1900 similar lines.

😬

As for other libdefs, Ville, I'm not sure. Usually when I've seen libdefs that have to deal with totally ambiguous output, it's any or Object or something similar.

@gantoine gantoine closed this May 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An addition to an existing component libdef Related to a library definition question
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants