4

In Typescript (specifically React with hooks), I'm trying to parse some URL hash data from an OAuth callback and utilize it in my components.

I'm able to parse my data by calling window.location.hash

const hash = window.location.hash.substr(1);
const oauthData = hash.split('&')
  .map(v => v.split('='))
  .reduce((pre, [key, value]) => (
    key == 'scope' ? {...pre, [key]: value.split('+')} : {...pre, [key]: value}
  ), {});
{
  "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMkJCWVkiLCJzdWIiOiI1TkZCTFgiLCJpc3MiOiJGaXRiaXQiLCJ0eXAiOiJhY2Nlc3NfdG9rZW4iLCJzY29wZXMiOiJyc29jIHJhY3QgcnNldCBybG9jIHJ3ZWkgcmhyIHJudXQgcnBybyByc2xlIiwiZXhwIjoxNTc4NTQ3NzkxLCJpYXQiOjE1NzgyMDQzOTF9.qLl0L5DthFu3NxeLodotPsPljYMWgw1AvKj2_i6zilU",
  "user_id": "5NFBLX",
  "scope": [
    "heartrate",
    "nutrition",
    "location",
    "sleep",
    "activity",
    "weight",
    "social",
    "profile",
    "settings"
  ],
  "token_type": "Bearer",
  "expires_in": "343400"
}

Awesome! Now I want to pass all this information into my component and this is where things get a little haywire and I can't figure out the way to get this data into my component because I break type-safety.

My component is built like this

export interface IOAuthProps {
  accessToken: string
  userID: string
  scope: string[]
  expiresIn: number
}

const OAuthFun: React.FC<IOAuthProps> = (props) => {

  const [ac] = useState(props.accessToken)
  return (
    <div>
      access token = {ac}
    </div>
  )
}

export default OAuthFun;

I've tried these permutations of what seem like the same thing (I'll omit the additional properties for brevity):

Nonworking example: can't even index oauthData because it is of type {}

<OAuthFun accessToken={oauthData['access_token'] as string}/>

Since I couldn't even index the raw json object as a dictionary, I figured I needed to create some type safety on the object getting constructed:

  const oauthData = hash.split('&')
    .map(v => v.split('='))
    .reduce((pre, [key, value]) => (
      key == 'scope' ? {...pre, [key]: value.split('+')} : {...pre, [key]: value}
    ), {access_token: String, user_id: String, scope: [], expires_in: Number});

However, this breaks the expression inside my reduce call: No overload matches this call. Which leads me to believe that I need to have some more concise manor of parsing the raw data, but I'm really unsure of how to do that.

I imagine I could cast it directly from raw data, to the interface but the raw data has underscore_casing instead of camelCasing for its naming conventions. Plus it just side-steps the problem without addressing it if I change the casing instead of learning how to normalize the data.

What is the correct approach to get raw data into the interface directly?

4
  • The correct approach is to have the interface declaration describe the data returned from the server. That means it needs to have the same property names. If property name normalization is desired, then you should perform it on the server. Remember, an interface declaration just describes the shape of data. Interfaces don't have any runtime Behavior. They just describe shapes Commented Jan 5, 2020 at 8:06
  • @AluanHaddad I would normalize it server-side, however it is not my service I am interacting with. It's actually FitBit's. And unfortunately I can't send the properties to my own server (which I tried first), because hash parameters aren't forwarded and have to be handled on the front end first (which brings us here) Commented Jan 5, 2020 at 8:10
  • I understand. So you need to name your typescript interface's properties to correctly reflect the shape of the data. That means using whatever naming convention is used by your data source. I agree it's not desirable to do this when you can avoid it, because it's good to be idiomatic and follow the language conventions like a camel casing but it doesn't sound like you have a choice Commented Jan 5, 2020 at 8:15
  • 1
    @AluanHaddad Ah okay, I see now! I'll post my solution, I can parse the data the way you describe and normalize it to match the conventions of my project. Thank you! Commented Jan 5, 2020 at 8:30

1 Answer 1

2

Based on the comments, I was able to piece together this solution.

import React from 'react';

export interface IOAuthProps {
  accessToken: string
  userID: string
  scope: string[]
  expiresIn: number
}

export function ParseOAuthProperties(rawHashProperties: string): IOAuthProps {
  const rawData = rawHashProperties.substr(1)
    .split('&')
    .map(v => v.split('='))
    .reduce((pre, [key, value]) => (
      {...pre, [key]: value}
    ), {access_token: "", user_id: "", scope: "", expires_in: ""});
    const normalizedData: IOAuthProps = {
      accessToken: rawData.access_token,
      userID: rawData.user_id,
      scope: rawData.scope.split('+'),
      expiresIn: Number(rawData.expires_in),
    }
    return normalizedData;
}

const OAuthFun: React.FC<IOAuthProps> = (props) => {
  return (
    <div>
      <div>access token = {props.accessToken}</div>
      <div>user id = {props.userID}</div>
      <div>scope = {props.scope}</div>
      <div>expires in = {props.expiresIn}</div>
    </div>
  )
}

export default OAuthFun;

Now I can take my method, which encapsulates the normalization and returns the interface, and use it from my parent component:

import React from 'react';
import OAuthFun, {ParseOAuthProperties, IOAuthProps} from './OAuthFun'

const App: React.FC = () => {
  const props: IOAuthProps = ParseOAuthProperties(window.location.hash)
  return (
    <div className="App">
      {/* Note, you can pass the interface wholesale with the spread operator */}
      <OAuthFun {...props} />
    </div>
  );
}

export default App;
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.