Flow Types with Promises (Fetch's) - javascript

I created a Fetch function to consume a JSON API and have defined types for the JSON object. I am confused about how to define the return type for the getCurrentJobAPI function since I do a bunch of .then() afterwards. Is the return value the last .then()? In my code, the last .then() is a setState, so what would the type be for that?
getCurrentJobAPI = (): {} => {
const url: string = `dummy_url&job_id=${this.props.currentJob}`;
return fetch(url, {credentials: 'include'})
.then((response) => {
return response.json();
})
.then((json: CurrentJob) => {
console.log(json);
const location = json.inventoryJob.location;
const ref_note = json.inventoryJob.note;
const id = json.inventoryJob.id;
const models = json.inventoryJobDetails.map((j) => {
return Object.assign({}, {
code: j.code,
qty: j.qty
})
});
this.setState({ currentCodes: models, location: location, ref_note: ref_note, id: id})
return json
})
.then((json: CurrentJob) => {
const barcodes = json.inventoryJob.history;
if (barcodes.length > 0) {
this.setState({apiBarcodes: barcodes})
}
this.calculateRows();
this.insertApiBarcodes();
this.setState({ initialLoad: true });
})
};
UPDATE:
Although I understand that I am supposed to define Promise<type> as the return value of getCurrentJobAPI (see Gilad's answer and comments), I am still unsure why I can't write Promise<CurrentJob> if the Fetch resolves as the JSON response.
[I have condensed my .then() statements per loganfsmyth's recommondation.]
Here are the type definitions for CurrentJob:
type Job = {
user_id: number,
status: 'open' | 'closed',
location: 'string',
history: {[number]: string}[],
note: string,
} & CommonCurrentJob;
type JobDetails = {
iaj_id: number,
code: number,
} & CommonCurrentJob;
type CommonCurrentJob = {
id: number,
qty: number,
qty_changed: number,
created_at: string,
updated_at: string
}

So first off, a disclaimer, I am a TypeScript user but I find that this question is actually applicable to both languages and has the same answer.
I created a Fetch function to consume a JSON API and have defined types for the JSON object. I am confused about how to define the return type for the getCurrentJobAPI function since I do a bunch of .then() afterwards. Is the return value the last .then()? In my code, the last .then() is a setState, so what would the type be for that?
TL;DR: Promise<void> (see note). As you suspect, this is in fact the return type of the last top-level .then in the promise chain.
Now lets dig a bit deeper
Here is your example, reworked very slightly to leverage type inference instead of annotating callback parameters that are declared as any by their receivers.
As an aside, these callback parameter annotations amount to unsafe implicit casts, or type assertions as we call them in TypeScript, and they lie about the shape of the code. They look like this
declare function takesFn(fn: (args: any) => any): void;
So I have minimized these since they form a subtle trap
// #flow
import React from 'react';
type CurrentJob = {
inventoryJob: Job,
inventoryJobDetails: JobDetails[]
}
export default class A extends React.Component<{currentJob:JobDetails}, any> {
getCurrentJobAPI: () => Promise<void> = () => {
const url = `dummy_url&job_id=${String(this.props.currentJob)}`;
return fetch(url, {credentials: 'include'})
.then(response => {
return (response : {json(): any}).json();
}) // --> Promise<any>
.then(json => {
const currentJob = (json: CurrentJob); // make the assumption explicit.
console.log(currentJob);
const {location, id, note: ref_note} = currentJob.inventoryJob;
const currentCodes = currentJob.inventoryJobDetails
.map(({code, qty}) => ({
code,
qty
}));
this.setState({currentCodes, location, ref_note, id});
return currentJob;
}) // --> Promise<CurrentJob>
.then(currentJob => {
const apiBarcodes = currentJob.inventoryJob.history;
if (apiBarcodes.length > 0) {
this.setState({apiBarcodes});
}
this.setState({initialLoad: true});
}); // --> Promise<void>
};
}
So I am making assertions about the promises in each then call above but those assertions are all validated by type inference with the exception of the initial type cast on the response value.
As further evidence, if we remove the type declaration from the getCurrentJobAPI property of A, flow will infer that its type is in fact Promise<void>.
Bonus: simplifying with async/await. I've used several ESNext features above to shorten the code and make it a bit more pleasant, but we can leverage a specific feature, async/await to make it easier to understand control flow and types in Promise based code.
Consider this revision.
// #flow
import React from 'react';
type CurrentJob = {
inventoryJob: Job,
inventoryJobDetails: JobDetails[]
}
export default class A extends React.Component<{currentJob:JobDetails}, any> {
getCurrentJobAPI = async () => {
const url = `dummy_url&job_id=${String(this.props.currentJob)}`;
const response = await fetch(url, {credentials: 'include'});
const json = await response.json();
const currentJob = (json: CurrentJob); // make the assumption explicit.
console.log(currentJob);
const {location, id, note: ref_note} = currentJob.inventoryJob;
const currentCodes = currentJob.inventoryJobDetails.map(({code, qty}) => ({
code,
qty
}));
this.setState({currentCodes, location, ref_note, id});
const apiBarcodes = currentJob.inventoryJob.history;
if (apiBarcodes.length > 0) {
this.setState({apiBarcodes});
}
this.setState({initialLoad: true});
};
}
Clearly, this is a void function. It has no return statements. However, as an async function, it inherently returns a Promise, just as it did when written as an explicit Promise chain.
Note: void is a construct that has been found useful in Flow and TypeScript to represent the semantic intent of function that do not return values but in reality such functions actually return undefined because, well, this is JavaScript. Flow does not seem to recognize undefined as a type, but under TypeScript, the function could equally be annotated as returning Promise<undefined>. Irregardless, Promise<void> is preferable thanks to the clarity of intent it provides.
Remarks: I worked through this using a combination of https://flow.org/try and the flow binary for Windows. The experience on Windows is really terrible and hopefully it will improve.

When chaining then's, the result will always be a promise.
When calling then, the return value is another promise, otherwise chaining then's wouldn't have been possible.
You can see that easily by using console.log() surrounding the entire chain.

Related

Typescript: handle data from service

I would like to display users' data, but I have a problem with displaying the correct profile pictures. If a user does not have a profile picture, I get "undefined" in the console. If one user has a profile picture, then the same picture will be displayed for all the users. I need help finding the error in my code.
export interface UserData {
id: number,
name: string
}
export interface UserWithImage extends UserData{
image?: string
}
export interface UserProfileImage {
id: number,
url: string
}
After I get the necessary data from the services, I try to push the profile image into the userData.
user-data.ts
userData: UserWithImage[];
userProfiles: UserProfileImage[];
userProfileImage: UserProfileImage[];
getUserData() {
this.userData = this.userService.getData();
this.userProfiles = await this.imagesService.getProfilePicture(this.userData?.map(u => u.id));
this.userProfileImage = this.userProfiles.filter(u => u.url);
this.userData?.forEach((data, i) => {
data.image = this.userProfileImage[i].url;
});
}
images.service.ts
public async getProfilePicture(ids: number[]): Promise<UserProfileImage[]> {
const toLoad = ids.filter(id => !this.userProfileImages.find(up => up.id === id)).map(u => u);
if (toLoad || toLoad.length) {
const loaded = (await firstValueFrom(this.httpClient.post<UserProfile[]>
(this.imgService.getServiceUrl(customersScope, `${basePath}settings/users/profil`), JSON.stringify(toLoad), {headers}))).map(sp => {
return {
id: sp.userId,
url: sp.profilepicId ? this.imgService.getServiceUrl(customersScope,
`${basePath}web/download/profilepic/${sp.profilepicId}/users/${sp.userId}`, true) : ''
} as UserProfileImage
});
this.userProfileImages = [...loaded, ...this.userProfileImages];
}
return this.userProfileImages;
}
user-data.html
<div ngFor="data of userData">
<etc-profil [name]="data.name" [image]="data.image"></etc-profil>
</div>
this.userData = this.userService.getData();
Is this an async function (i.e. are you missing an await)?
this.userProfiles = await this.imagesService.getProfilePicture(this.userData?.map(u => u.id));
This line would fail is this.userData is a promise. this.userProfiles would be undefined due to the use of optional chaining (?.)
this.userProfileImage = this.userProfiles.filter(u => u.url);
This line appears to do nothing, the filter predicate is saying that anything with a url property that is not null or undefined is included, but the interface says that url is non-optional and doesn't support null or undefined.
this.userData?.forEach((data, i) => {
data.image = this.userProfileImage[i].url;
});
Again, if this.userData is a promise, this will do nothing due to the optional chaining.
If it does run, its assumed that there is a one-to-one relationship between users and profile images (index count and order must be the same).
I didn't consider the implementation of getProfilePicture because I think these issues need resolving first.

Can't get data from .pipe(map())

Got some problems with observable.
I have a function, with one returns me an Observable.
public getData(userId) {
const data = this.execute({userId: userId});
return {event: "data.get", data: data}
}
private execute(input: SomeDto): Observable<SomeRefType[]> {
return this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
}).pipe(map(res => res));
}
Type what pipe(map) returns
What I'm got when trying to return or log data
Question: Why .pipe(map(res => res)) don't work? What am I doing wrong?
For sure, I can read data from .pipe(take(1)).subscribe(data => console.log(data)), but, how can I return data from construction like this?
Thanks everyone! Have a good day!
As said in the Rxjs documentation observable are lazy computation.
It means the as long as you don't subscribe to them they won't do anything. It exists two ways to trigger a subscription.
Either within a ts file using .susbcribe() or within a view when calling an endpoint.
If you're using nestjs it would be when calling the url defined within a #Controller('') with an http verb like #Get('path')
By convention you suffix observable variables with $: data$ = new Observable<any>().
At some point you'll have to convert the observable to a promise. Best to do it early and convert it using firstValueFrom immediately after the query.
Then convert your caller to an async method to use the returned value.
public async getData(userId): Promise<{ event: string, data: SomeRefType[] }> {
const data = await this.execute({userId: userId});
return { event: 'data.get', data };
}
private execute(input: SomeDto): Promise<SomeRefType[]> {
const res$ = this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
});
return firstValueFrom(res$);
}

Modify Observable with another Observable and return the initial Observable

I want to return a ClientDetails object with a loaded image.
So retrieve an Observable, and modify the value with another Observable and return the whole Observable.
I hope the code below indicates what I am trying to do, but I know it can be done much cleaner using RxJS operators. Anyone know how to?
interface ClientDetails {
team: Member[];
}
interface Member {
id: number;
image: string;
}
this.clientDetails$ = this.clientService.getClientDetails().subscribe((details: ClientDetails) => {
details.team.forEach(member => {
this.imageService.getImage(member.id).subscribe((image: string) => {
member.image = image
}
}
}
You're right in assuming RxJS operators would make it more elegant. At the moment the variable this.clientDetails$ doesn't hold an observable and it wouldn't work as you'd expect it to.
Instead you could use higher order mapping operator switchMap to switch from one observable to another (it's better to avoid nested subscriptions in general) and forkJoin function to trigger multiple observables in parallel. You could also use JS destructing and RxJS map operator to return the object with all it's contents.
Try the following
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) =>
forkJoin(
details.team.map(member =>
this.imageService.getImage(member.id).pipe(
map(image: string => ({...member, member.image: image}))
)
)
).pipe(
map((team: any) => ({...details, details.team: team}))
)
);
);
Note: I didn't test the code. Please check if the object returned is what you actually require.
Try
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) => {
const images$: Observable<string[]> = forkJoin(
details.team.map(member => this.imageService.getImage(member.id))
);
return forkJoin([of(details), images$]);
}),
map(([details, images]) => {
return {
team: _.zipWith(details.team, images, (d, m) => d.image = m) // zipWith = lodash function
};
}),
).subscribe((details: ClientDetails) => console.log(details));

Default string value after call the object in JavaScript

I have a js object in which I return my endpoint addresses from api. This is a very nice solution for me, it looks like this:
export const API_BASE_URL = 'http://localhost:3000';
export const USERS = '/Users';
export default {
users: {
checkEmail: (email) => `${API_BASE_URL}${USERS}/${email}/checkEmail`,
notifications: `${API_BASE_URL}${USERS}/notifications`,
messages: `${API_BASE_URL}${USERS}/messages`,
},
};
Now I can call this address in my redux-saga to execute the xhr query:
import { api } from 'utils';
const requestURL = api.users.notifications;
But I'm a bit stuck because now I have a problem - base path is missing here: '/users'.
Now when I call api.users, then I get a object. I would like to have a default value after calling the object like:
import { api } from 'utils';
const requestURL = api.users; // http://localhost:3000/Users
const requestURL2 = api.users.notifications; // http://localhost:3000/Users/notifications
I know that I could add a new string with the name 'base' to the object and add '/Users' there, but I don't like this solution and I think, there is a better solution.
You could do one of the following:
extend the String class
const API_BASE_URL = "http://localhost:3000"
const USERS = "/Users"
class UsersEndpoints extends String {
constructor(base) {
super(base)
}
// this is still a proposal at stage 3 to declare instance variables like this
// if u want a truly es6 way you can move them to the constructor
checkEmail = (email) => `${API_BASE_URL}${USERS}/${email}/checkEmail`
notifications = `${API_BASE_URL}${USERS}/notifications`
messages = `${API_BASE_URL}${USERS}/messages`
}
// you can use userEndpoints itself as a string everywhere a string is expected
const userEndpoints = new UsersEndpoints(API_BASE_URL)
export default {
users: userEndpoints
}
The previous is just actually equivalent to
...
const userEndpoints = new String(API_BASE_URL)
userEndpoints.notifications = `${API_BASE_URL}${USERS}/notifications`
...
Obviously this is not recommended: you should not extend native classes, there are many disadvantages to this approach.
An obvious example is that there could be a conflict between the properties you use and the properties that might be brought by the native class
override the toString method
...
export default {
users: {
checkEmail: (email) => `${API_BASE_URL}${USERS}/${email}/checkEmail`,
notifications: `${API_BASE_URL}${USERS}/notifications`,
messages: `${API_BASE_URL}${USERS}/messages`,
toString: () => API_BASE_URL
},
};
// this is actually not much different than the previous method, since a String is an objet with an overridden toString method.
// That said this method is also not recommended since toString is used in many places in native code, and overriding it just to substitute a string value will make information get lost in such places, error stacks for example
Achieve what u want using the language features intended for such a use case
What you are asking is to make the same variable to have different values in the same time, which is not possible in the language syntax, and it makes sense because it makes it hard to reason about code.
that being said i recommend something of the following nature
// it is also better to use named exports
export const getUsersEndpoint = ({
path = "",
dynamicEndpointPayload = {},
} = {}) => {
switch (path) {
case "notifications":
return `${API_BASE_URL}${USERS}/notifications`
case "messages":
return `${API_BASE_URL}${USERS}/messages`
case "checkEmail":
return `${API_BASE_URL}${USERS}/${dynamicEndpointPayload.email}/checkEmail`
// you still can do checkEmail like this, but the previous is more consistent
// case "checkEmail":
// return (email) => `${API_BASE_URL}${USERS}/${email}/checkEmail`
default:
return `${API_BASE_URL}`
}
}
// you can use it like this
getUsersEndpoint() // returns the base
getUsersEndpoint({path: 'notifications'})
You can extend prototype to achieve this behaviour:
export const API_BASE_URL = 'http://localhost:3000';
export const USERS = '/Users';
const users = `${API_BASE_URL}${USERS}`
const baseUrls = {
checkEmail: (email) => `${users}/${email}/checkEmail`,
notifications: `${users}/notifications`,
messages: `${users}/messages`,
}
Object.setPrototypeOf(users.__proto__, baseUrls);
export default {
users
};
Try having object will all user endpoint and a function that return a value of a end point
const user = {
default: '/users',
notification: '/notification',
profile: '/profile',
getEndPoint(prop) {
if(this[prop] === 'default' ){
return this[prop];
} else {
if(this[prop]) {
return this.default + this[prop];
}
}
}
}
So you can have more end points that come under user and you can simply call
const requestURL = api.user.getEndPoint('default'); // http://localhost:3000/Users
const requestURL2 = api.user.getEndPoint('notifications'); // http://localhost:3000/Users/notification

How to make a Promise not re-run code when already resolved?

I have a User Model:
export class User extends Serializable{
id: string;
first_name: string;
middle_name: string;
last_name: string;
email: string;
image_url: string;
// mykeyStore
static store:StoreDatabase = new StoreDatabase("mykeyStore");
... More code...
and a loadProfile() function to that class which returns a promise.
loadProfile():Dexie.Promise<any>{
let promise = User.store.get('user')
.then(
tuple => {
// Extract User Data from tuple
let user_data = tuple && tuple.value
// Fill User attribute for Tuple's value
for (var attr in user_data) {
this[attr] = user_data[attr];
}
});
return promise;
}
How can I structure my code, so that calling loadProfile will not always run the then if it is already resolved, by calling the following:
let user = new User();
user.loadProfile().then( () =>
console.log(user.first_name)
);
In your case, loadProfile will execute inside code again. To avoid that, you need to store the promise in some variable. For example (es6, cuz i don't know typescript well):
// Execute code inside loadProfile and store the promise in some variable
const loadProfilePromise = user.loadProfile();
// Will show Hurray
loadProfilePromise.then(() => console.log('Hurray');
// Will not execute code inside loadProfile again, but will show 'Hurray2'
setTimeout(() => {
loadProfilePromise.then(() => {
console.log('Hurray2'));
});
}, 100);
And also, do not forget to handle exceptions and rejects in promises, and log them ;D
Every time you call user.loadProfile(), a new promise is created. Therefore, the code in .then() will always run.
If you want to avoid loading attributes multiple times, you can change your function to this:
loadProfile():Dexie.Promise<any>{
if (this.promise) return this.promise
this.promise = User.store.get('user')
.then(
tuple => {
// Extract User Data from tuple
let user_data = tuple && tuple.value
// Fill User attribute for Tuple's value
for (var attr in user_data) {
this[attr] = user_data[attr];
}
});
return this.promise;
}
Then if you call user.loadProfile() when it's already loaded, the code in .then() will be executed immediately.

Categories

Resources