HttpClient Angular 9 converting nested json to nested object - javascript

I'm trying to see if there's a more elegant way other than using map to accomplish the following:
I have a class with an array of another object. So my Meeting object has many users:
import { User } from './user';
export class Meeting {
_id?: string;
name: string;
roomSid?: string;
users: User[];
contentList: any;
startDate: Date;
endDate: Date;
createDate: Date;
}
export class User {
_id: string;
firstName: string;
lastName: string;
dealerName: string;
location: string;
role: 'attendee' | 'host' | 'participant';
videoServiceId: string;
profilePic?: string;
__v?: string;
get isHost(): boolean {
return this.role === 'host';
}
get isAttendee(): boolean {
return this.role === 'attendee';
}
get isParticipant(): boolean {
return this.role === 'participant';
}
get isClient(): boolean {
return this.isAttendee || this.isParticipant;
}
}
When I do:
this.httpClient
.get<Meeting>(`${this.apiUrl}${meetingId}`)
.pipe(
catchError(this.handleError));
without using map to convert the received result from the get, the User object array is getting set incorrectly. My getters are undefined unless I do a Object.assign(new User, user). HttpGet returns the Meeting as a Meeting class just fine, just not the objects inside.
Is there something I'm missing or is this working as intended and I have no choice but to use map(). Although I feel that if I have another Object inside my User class, then things could get a bit messier.
For reference here is a response from the server:
{
"users": [
{
"_id": "971c4160-c60c-11ea-8505-dff43e61059c",
"firstName": "alex",
"lastName": "oroszi",
"dealerName": "here",
"location": "here",
"role": "attendee",
"videoServiceId": "PA634a9331a9cad648bb6e6dbcea8e49a0"
}
],
"contentList": [],
"_id": "5f0a005b627fb647f519118b",
"name": "Room1",
"startDate": "2020-07-11T16:00:00.000Z",
"endDate": "2020-07-11T17:00:00.000Z",
"createDate": "2020-07-11T18:09:31.016Z",
"__v": 0
}

When specifying .get<Meeting>(`${this.apiUrl}${meetingId}`) you're letting TypeScript know that the get method will return a Meeting object which is not the case. If you check the result of your method with data instanceof Meeting it will return false. You will need to create a meeting object and populate it with the data received from the request.
The easiest way to do this without any libraries will probably be as follows.
export class Meeting {
_id?: string;
name: string;
roomSid?: string;
users: User[];
contentList: any;
constructor(data) {
this._id = data._id;
this.name = data.name;
this.roomSid = data.roomSid;
this.users = data.users.map(user => new User(user));
this.contentList = data.contentList;
// ...
}
}
export class User {
_id: string;
firstName: string;
lastName: string;
dealerName: string;
location: string;
role: 'attendee' | 'host' | 'participant';
videoServiceId: string;
profilePic?: string;
__v?: string;
constructor(data) {
this._id = data._id:
this.firstName = data.firstName:
// ...
}
get isHost(): boolean {
return this.role === 'host';
}
get isAttendee(): boolean {
return this.role === 'attendee';
}
get isParticipant(): boolean {
return this.role === 'participant';
}
get isClient(): boolean {
return this.isAttendee || this.isParticipant;
}
}
function getMeeting(meetingId): Observable<Meeting> {
this.httpClient.get<Meeting>(`${this.apiUrl}${meetingId}`)
.pipe(
catchError(this.handleError),
map(meeting => new Meeting(meeting)),
);
}
The downside of this approach is the DRYness as the property name is repeated three times. An alternative is solutions such as TypedJson which allow you to serialize and deserialze objects with help of decorators. Quick example of a model in TypedJSON:
#jsonObject
class Meeting {
#jsonMember
_id: string;
#jsonArrayMember(User)
users: Array<User>;
}

Related

Getting error every time after assign interface to Object - Typescript

Getting error after assigning an interface to any object in typescript.
I have two below interface.
export interface UserInfo {
firstName: string,
lastName: string,
lastUpdateTime: string,
status: string
}
export interface User{
activeCount: string,
subsource: string,
users: UserInfo[]
}
After assign the above User interface to an object in a component getting error.
user: User= {}; // Type {} is missing the properties
user: User;
You can make properties optional by using the Partial utility type.
const user: Partial<User> = {};
Or do the same thing manually by mapping the properties.
interface UserInfo {
firstName: string;
lastName: string;
lastUpdateTime: string;
status: string;
}
interface User {
activeCount: string;
subsource: string;
users: UserInfo[];
}
export type MakeOptional<T> = { [K in keyof T]?: T[K] };
const user: MakeOptional<User> = {};
This would be a more generic solution for your problem.
Its because when you specify a property of the type like this firstName: string the property should be present (its required) please change its definition to firstName?: string, which means the property is an optional property and can be not defined!
Then the error will go away!
export interface UserInfo {
firstName?: string,
lastName?: string,
lastUpdateTime?: string,
status?: string
}
export interface User{
activeCount?: string,
subsource?: string,
users?: UserInfo[]
}
After assign the above User interface to an object in a component getting error.
user: User= {}; // Type {} is missing the properties - wont come now!
user: User;
Of course, you need to add the properties to the object:
const user: User = {
users: [],
activeCount: "",
subsource: "",
};
I think you have to tell the compiler that {} is a type of User and contains the same properties as the User interface. For example:
user:User = {} as User;
as per typescript,
You Define user: User it set value of user as User
to use interface you have to define like : let user: User

Property doesn't exist on one type in union of two types based on a generic

I have a method:
async findUserById<T extends Partial<UserData>>(
id: number,
select?: (keyof UserData)[]
) {
if (!select) {
select = ["id", "name", "email", "picture", "bio"];
const user = await this.findOne(id, { select });
return user as UserSafeData;
}
const user = await this.findOne(id, { select });
return user as unknown as T;
}
It decides what to return based on select parameter and if a generic is passed it sets the return type to it, if no select parameter it returns UserSafeData type.
1- Generic type like:
const user = await this.userRepository.findUserById<{ picture: string; }>(userId, ["picture"]);
2- UserSafeData which is:
type UserSafeData = {
id: number;
email: string;
name: string;
picture: string;
bio: string;
tasks: Task[];
}
Now, when I try to do:
const user = await this.userRepository.findUserById<{
id: number;
email: string;
refreshToken: string;
}>(userId, ["id", "refreshToken", "email"]);
if (!user?.refreshToken) ...
When trying to reference refreshToken I get this error:
Property 'refreshToken' does not exist on type 'UserSafeData | { id: number; email: string; refreshToken: string; }'.
Property 'refreshToken' does not exist on type 'UserSafeData'.
This is because refreshToken is not in the UserSafeData type but it should work because it exists on the generic type.
So, how to solve it?
Note: why I am doing this is because I am trying to achieve a strongly static typed database calls because if I select only one entity column, typescript will still assume that I have all of the entity model

How to define an unknown object property key in typescript

With the Vue composition API we created the following composable:
import { computed, reactive, SetupContext, ref } from '#vue/composition-api'
export const useApplications = (root: SetupContext['root']) => {
const applications = reactive({
1: {
name: ref(root.$t('app1.name')),
description: ref(root.$t('app1.description')),
formName: 'app1form',
},
2: {
name: ref(root.$t('app2.name')),
description: ref(root.$t('app2.description')),
formName: 'app2form',
},
})
const getApplication = (id: string) => {
return applications[id]
}
return {
applications: computed(() => applications),
getApplication,
}
}
Although the code works fine it generates the TS error:
#typescript-eslint/no-unsafe-return: Unsafe return of an any typed value
When hovering over the applications section it's clear that typescript recognizes the types except for the property name (1):
Do we need to create an interface to solve this and do we have to redefine each and every property in the interface? I tried something like this but it is incorrect:
interface IApplication {
[key]: string {
name : string
description: string
formName: string
}
}
TypeScript doesn't generally type things so they can be indexed by any string key. As you say, you can define an interface for it:
interface IApplications {
[key: string]: {
name : string;
description: string;
formName: string;
};
}
// ...
const applications: IApplications = ...
Or you might just use a type for the object part of that and use the built-in Record type for applications:
interface IApplication {
name : string;
description: string;
formName: string;
}
// ...
const applications: Record<string, IApplication> = ...
Or combining the two:
interface IApplication {
name : string;
description: string;
formName: string;
}
type IApplications = Record<string, IApplication>;
(Or you can inline the IApplication part. Or... :-) )
According to the structure of the parameters of reactive function, the interface could be defined like :
interface IApplication {
[key:string]:{
name : string
description: string
formName: string
}
}
and
const applications = reactive<IApplication>({ ...
But I want to suggest another approach which define tha applications as reactive parameter which has an array as value:
import { computed, reactive, SetupContext, ref ,toRef} from '#vue/composition-api'
interface IApplication {
name : string
description: string
formName: string
}
export const useApplications = (root: SetupContext['root']) => {
const state= reactive<Array<IApplication>>({ applications :
[{
name: ref(root.$t('app1.name')),
description: ref(root.$t('app1.description')),
formName: 'app1form',
},
{
name: ref(root.$t('app2.name')),
description: ref(root.$t('app2.description')),
formName: 'app2form',
}],
})
const getApplication = (index: number) => {
return state.applications[index]
}
return {
applications: toRef(state,'applications'),
getApplication,
}
}

Typescript, check if request body has type User otherwise throw error

I'm stuck trying to solve a problem. I'm using express js to build a rest api. I want the user to be able to update their profile.
I've created a User model:
export type User = {
email: string
creation_date: number
first_name?: string
last_name?: string
payment_detals?: {
iban: string
last_updated: string
}
address?: {
city: string
street: string
house_number: string
postal_code: string
}
products?: string[]
}
But I want to receive the request body and update the value for that user in the database (No SQL, Firebase). But I don't want the user to add fields which are not specified in the User type.
How do I check if the request body has type User, if not throw an error?
The route:
const edit = async (req: Request, res: Response) => {
try {
let data = req.body
if (data instanceof User)
} catch (err) {
return res.status(501).json({ error: err.message })
}
return res.status(200).json({ status: 'ok' })
I can't find any help on the internet, so maybe someone could help me out?
So for example, if the payload of the post request is:
{
"name": "Jack"
}
It should throw an error, because name is not a member of User.
How can I solve this? All help is appreciated!
Updated now trying with classes:
export class CUser {
email: string
creation_date: number
first_name?: string
last_name?: string
payment_detals?: {
iban: string
last_updated: string
}
address?: {
city: string
street: string
house_number: string
postal_code: string
}
products?: string[]
}
The route
const edit = async (req: Request, res: Response) => {
let data = req.body
console.log(data instanceof CUser)
return res.status(200).json({ status: 'ok' })
}
When the request.body is:
{
"email": "mike#gmail.com",
"creation_date": 849349388935
}
The data instanceof CUser will always result to false. Wait is it maybe because data is an object?..
Types or interfaces that you define in Typescript are stripped when it's converted into Javascript, so you won't be able to able to check the type during runtime.
What you'll need to do is create a type-guard function that asserts true or false whether or not your request has those specific User properties.
For a good example see: How to check the object type on runtime in TypeScript?
You can create a constructor or function in a typescript class , which will take the req.body and only pick the required keys from the object, assign to this member variable and return you a new instance of the User object.
Now you can apply the checks on User instance or also can create a validateObject method inside the User class
I've solved this by writing a function which compares the request body with the types that I expect, and it works great! If there is something wrong with the fields or the required is wrong, the server will throw an error immediately. Here are some code snippets:
The functions
export type Schema = {
fields: { [key: string]: string }
required?: string[]
}
const required = (obj: any, required: string[]) => {
for (let key of required) {
if (obj[key] === undefined) return false
}
return true
}
export const validate = async (obj: any, model: Schema) => {
if (model.required) {
const status = required(obj, model.required)
if (!status) return false
}
for (let key of Object.keys(obj)) {
if (model.fields[key] === undefined) return false
else if (typeof obj[key] !== model.fields[key]) return false
}
return true
}
Example type
import { Schema } from './'
export type User = {
email: string
creation_date: number
subscription: string
first_name?: string
last_name?: string
payment_detals?: {
iban: string
last_updated: string
}
address?: {
city: string
street: string
house_number: string
postal_code: string
}
categories?: string[]
products?: {
ean: number
category?: string
notes?: string
}[]
}
export const UserSchema: Schema = {
fields: {
email: 'string',
subscription: 'string',
first_name: 'string',
last_name: 'string',
payment_details: 'object',
address: 'object',
categories: 'object',
products: 'object',
},
required: ['email']
}
On the server
let status = await validate(body, UserSchema)
if (!status) return res.status(422).json(Message.error.wrong_request_body)
// else continue

Angular5 - Error: Function DocumentReference.set() called with invalid data. Data must be an object, but it was: a custom PlatformModel object

I find allready some posts on google where people solve this problem. but i cant reproduce the solutions on my project.
My Interface:
declare module PlatformInterface {
export interface Design {
primaryColor: string;
backgroundImage: string;
}
export interface Saga {
id: string;
name: string;
short_desc: string;
desc: string;
manga: Manga[];
anime: Anime[];
}
export interface Root {
id: string;
name: string;
design: Design[];
saga: Saga[];
}
}
My Model:
export class PlatformModel implements PlatformInterface.Root {
id: string;
name: string;
design = [];
saga = [];
constructor(obj?: any) {
this.id = obj.name.toLowerCase().replace(' ', '-');
this.name = obj.name;
this.design = obj.design;
this.saga = obj.saga;
}
}
My Service:
#Injectable()
export class PlatformService {
public list$: Observable<PlatformModel[]>;
private _platform: AngularFirestoreCollection<PlatformModel>;
constructor(db: AngularFirestore) {
this._platform = db.collection<PlatformModel>('platforms');
this.list$ = this._platform.valueChanges();
}
/** Get Platform by id */
get(id: string): Observable<PlatformModel> {
return this._platform.doc<PlatformModel>(id).valueChanges();
}
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(platforms));
}
/** Remove Platform */
remove(id: string) {
return fromPromise(this._platform.doc(id).delete());
}
}
My function in Component.ts
constructor(public _platformService: PlatformService) {
}
addPlatform(name: string) {
if (name !== '') {
const platform = new PlatformModel({
name: name,
design: [],
saga: []
});
this._platformService.set(platform.id, platform).subscribe();
}
}
The Angular Compiler dont Throw any error, But when i try to fire the addPlatform Function i get in Browser this error:
ERROR Error: Function DocumentReference.set() called with invalid data. Data must be an object, but it was: a custom PlatformModel object
The Errors Says that the Data must be an object, but it is allready an object or not? i mean i define in the service it with:
public list$: Observable<PlatformModel[]>;
[] Makes it to an object or not?
I've found some clarification here Firestore: Add Custom Object to db
while firebase could send the data inside your object to the database, when the data comss back it cannot instantiate it back into an instance of your class. Therefore classes are disallowed
my workaround for custom class was
this.db.collection(`${this.basePath}/`).doc(custom_class.$key)
.set(Object.assign({}, JSON.parse(JSON.stringify(custom_class))))
.then( ret => {
log.debug('file added', ret);
}).catch( err => {
log.error(err);
});
so I guess in your case it would be
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(Object.assign({},JSON.parse(JSON.stringify(platforms))));
}
For adding a Map into Firestore document you'll have to use:
Object.assign({}, JSON.parse(JSON.stringify(YOUR_MAP)))

Categories