TypeScript: parse raw data into interface - json

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?

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;

Related

How to dynamically create a typescript type from a static json schema that specifies types as strings

If I have a JSON schema saved in a file like f1040.json with content:
[
{
name: "L1",
type: "String"
},
{
name: "C2",
type: "Boolean"
},
{
name: "L3",
type: "String"
},
...
]
And I want to generate a type that looks like:
type F1040 = {
L1: string;
C2: boolean;
L3: string;
...
}
How can I generate this without specifying each field manually (there are hundreds of them)? My first attempt at a solution isn't valid typescript (bc I'm providing more then one mapped type I think) but hopefully this invalid example clarifies what I'm trying to do:
import { f1040 } from "./f1040.json";
const bools = f1040.filter(e => e.type === "Boolean").map(e => name) as string[];
const strings = f1040.filter(e => e.type === "String").map(e => e.name) as string[];
export type F1040 = {
[key in (typeof bools)[number]]?: boolean;
[key in (typeof strings)[number]]?: string;
};
My incorrect solution was inspired by an answer to a similar but different question: TS create keys of new type from array of strings
Edit1: The solution doesn't need to be dynamic but if it's a build-time solution then it needs to play nice with rollup & the declarations bundler I'm using
Edit2: Another unsuccessful attempt, this time utilizing #sinclair/typebox:
import { Static, Type } from "#sinclair/typebox";
import { f1040 } from "./f1040.json";
const F1040 = Type.Object({
...f1040
.filter(e => e.type === "Boolean")
.map(e => e.name)
.reduce((res, f) => ({ ...res, [f]: Type.Boolean() }), {}),
...f1040
.filter(e => e.type === "String")
.map(e => e.name)
.reduce((res, f) => ({ ...res, [f]: Type.String() }), {}),
});
export type F1040 = Static<typeof F1040>;
const f1040Data = {
L1: true,
C2: "Yea",
} as F1040
The above attempt builds fine w/out any error.. which is too bad because the type assignments at the end are wrong. This should fail to build with a TypeError saying something like
Types of property 'L1' are incompatible. Type 'boolean' is not comparable to type 'string'.
It cannot be done dynamically, because your typescipt program is compiled into javascript before it is run, and in javascript all type information is removed. Types are only used in the typescript compilation process.
So you need to have the types before typescript compilation. E.g. using https://www.npmjs.com/package/json-schema-to-typescript

Property does not exist on type 'never' on JSON array

I'm just trying to fetch some JSON data from a url. The JSON data is formatted like so (reduced to two entries for simplicity):
[
{
"id": 1
"name": "Brett",
"gender": "male"
},
{
"id": 2
"name": "Sandra",
"gender": "female"
}
]
I can print profiles using console.log(profiles) and see all the entries in the console, but when i try to access the .name field i get the error
Property 'name' does not exist on type 'never'.
Here is the code for the app:
const URL = 'someurl'
function App() {
const [curId, setId] = useState(0);
//const [curProfile, setCurProfile] = useState(undefined);
const [profiles, setProfiles] = useState([])
useEffect(() => {
fetch(URL)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Something went wrong!");
}
})
.then(
(response) => {
setProfiles(response);
setId(1);
//setCurProfile(profiles[curId - 1]);
})
.catch((error) => {
console.log(error)
})
}, []);
return (
<div className="App">
<p>
{profiles[curId].name}
</p>
</div>
);
}
export default App;
Also as a side question, I'm having some problems storing the current profile in the curProfile variable. Could someone point me in the right direction for that? Thanks!
The initial state of profiles is empty array and curId is 0, so profiles[curId] should be undefined thus profiles[curId].name would be error as initial rendering.
You should always check if profiles is empty or not.
return (
<div className="App">
{profiles.length > 0 &&
<p>
{profiles[curId].name}
</p>
}
</div>
)
You've got to type your state, otherwise Typescript won't know what to expect. You also need to type the response.
Something like:
type Profile = {
id: number,
name: string,
gender: string
}
const [profiles, setProfiles] = useState <Profile[]> ([]);
(...)
setProfiles(response as Profile[]);

Angular 2 - Getting object id from array and displaying data

I currently have a service that gets an array of json objects from a json file which displays a list of leads. Each lead has an id and when a lead within this list is clicked it takes the user to a view that has this id in the url ie ( /lead/156af71250a941ccbdd65f73e5af2e67 )
I've been trying to get this object by id through my leads service but just cant get it working. Where am I going wrong?
Also, i'm using two way binding in my html.
SERVICE
leads;
constructor(private http: HttpClient) { }
getAllLeads() {
return this.http.get('../../assets/leads.json').map((response) => response);
}
getLead(id: any) {
const leads = this.getAllLeads();
const lead = this.leads.find(order => order.id === id);
return lead;
}
COMPONENT
lead = {};
constructor(
private leadService: LeadService,
private route: ActivatedRoute) {
const id = this.route.snapshot.paramMap.get('id');
if (id) { this.leadService.getLead(id).take(1).subscribe(lead => this.lead = lead); }
}
JSON
[
{
"LeadId": "156af71250a941ccbdd65f73e5af2e66",
"LeadTime": "2016-03-04T10:53:05+00:00",
"SourceUserName": "Fred Dibnah",
"LeadNumber": "1603041053",
},
{
"LeadId": "156af71250a999ccbdd65f73e5af2e67",
"LeadTime": "2016-03-04T10:53:05+00:00",
"SourceUserName": "Harry Dibnah",
"LeadNumber": "1603021053",
},
{
"LeadId": "156af71250a999ccbdd65f73e5af2e68",
"LeadTime": "2016-03-04T10:53:05+00:00",
"SourceUserName": "John Doe",
"LeadNumber": "1603021053",
}
]
You didn't used the newly created leads array (const leads is not this.leads), so do this:
getLead(id: any) {
return this.getAllLeads().find(order => order.LeadId === id);
}
And change your map to flatMap, because from the server you get an array, but you have to transform it to a stream of its items:
getAllLeads() {
return this.http.get('../../assets/leads.json').flatMap(data => data);
}
Don't forget to import it if you have to: import 'rxjs/add/operator/flatMap';
You can have getLead in your component level itself since you are not making any api to get the information. In your component,
this.lead = this.leads.find(order => order.id === id);
or to make the above service work, just do leads instead of this.leads
const lead = leads.find(order => order.id === id);

Angular 2+ http get() JSON-data. Cannot .filter data

I have a service.ts, where I use the get() method (http) to get data from a local JSON-file. It Works fine.
getMusic() {
return this.http.get('assets/music.json')
.map((res:Response) => res.json())
In my class I have my 'suscribe':
ngOnInit() {this.musicService.getMusic().subscribe(
data => { this.music = data.music}
);
}
My JSON-file looks like this:
{
"music" : [
{
"no": 11,
"name": "Music 1",
}, ...... aso....
Everything is perfect. Well now I'm trying to filter my data, so it only shows music with no=11.
I have this Imports:
//import 'rxjs/add/operator/map';
//import 'rxjs/add/operator/filter';
import 'rxjs/Rx';
I have tried several things. Including filtering for a number instead of a string, using == instead of ===, putting .filter inside or outsite .map aso. When I try this I receive no result at all. I get no errors, just no result:
getMusic() {
return this.http.get('assets/music.json')
.map((res:Response) => res.json())
.map(x => x.filter(y => y.name === 'Music 1'));
}
You have wrong filter operator function. It should be filtering on music.
getMusic() {
return this.http.get('assets/music.json')
.map((res:Response) => res.json())
//filter on `music` object instead of `x` directly
.map(x => x.music.filter(y => y.name === 'Music 1'));
}
the filter function is applied to an array not to an instance of it
try using
this.filteredArr = this.music.filter(x=>x.name == 'music 1'); //some condition

How to alter keys in immutable map?

I've a data structure like this (generated by normalizr):
const data = fromJS({
templates: {
"83E51B08-5F55-4FA2-A2A0-99744AE7AAD3":
{"uuid": "83E51B08-5F55-4FA2-A2A0-99744AE7AAD3", test: "bla"},
"F16FB07B-EF7C-440C-9C21-F331FCA93439":
{"uuid": "F16FB07B-EF7C-440C-9C21-F331FCA93439", test: "bla"}
}
})
Now I try to figure out how to replace the UUIDs in both the key and the value of the template entries. Basically how can I archive the following output:
const data = fromJS({
templates: {
"DBB0B4B0-565A-4066-88D3-3284803E0FD2":
{"uuid": "DBB0B4B0-565A-4066-88D3-3284803E0FD2", test: "bla"},
"D44FA349-048E-4006-A545-DBF49B1FA5AF":
{"uuid": "D44FA349-048E-4006-A545-DBF49B1FA5AF", test: "bla"}
}
})
A good candidate seems to me the .mapEntries() method, but I'm struggling on how to use it ...
// this don't work ... :-(
const result = data.mapEntries((k, v) => {
const newUUID = uuid.v4()
return (newUUID, v.set('uuid', newUUID))
})
Maybe someone can give me a hand here?
mapEntries is the correct method. From the documentation, the mapping function has the following signature:
mapper: (entry: [K, V], index: number, iter: this) => [KM, VM]
This means that the first argument is the entry passed in as an array of [key, value]. Similarly, the return value of the mapper function should be an array of the new key and the new value. So your mapper function needs to look like this:
([k, v]) => {
const newUUID = uuid.v4()
return [newUUID, v.set('uuid', newUUID)]
}
This is equivalent to the following (more explicit) function:
(entry) => {
const key = entry[0]; // note that key isn't actually used, so this isn't necessary
const value = entry[1];
const newUUID = uuid.v4()
return [newUUID, value.set('uuid', newUUID)]
}
One thing to note is that the templates are nested under the templates property, so you can't map data directly -- instead you'll want to use the update function.
data.update('templates', templates => template.mapEntries(...)))
So putting everything together, your solution should look like the following:
const result = data.update('templates', templates =>
templates.mapEntries(([k, v]) => {
const newUUID = uuid.v4()
return [newUUID, v.set('uuid', newUUID)]
})
);