I'm using React.js and Typescript and in the React.js store I store Javascript objects. Sometimes they're sent by the server, so they're just objects, they have no member functions. But I want member functions, so that instead of this:
// Interface and "external" member function for an object in the store:
interface User {
id: UserId;
isAdmin?: boolean;
isModerator?: boolean;
...
}
function isStaff(user: User) {
return user.isAdmin || user.isModerator;
}
if (isStaff(user)) {
showPowerOffDataCenterButton();
}
I can do this:
if (user.isStaff()) {
...
Is there any React or Javascript features or Typescript syntactic-sugar magic that can add member functions to the React store data structures? Please note that the objects are sometimes sent from the server as JSON and parsed with JSON.parse(..), so I don't think I can declare my own Javascript classes and add functions to their .prototype field (becasue I don't control the creation of the objects).
(I'm planning to use Redux later + some immutable-JS library, in case that matters)
Or if not possible, any workarounds?
You could just make a class that contains that information instead of having them in separate functions.
interface UserInfo {
id: string;
isAdmin?: boolean;
isModerator?: boolean;
}
class User {
constructor(private user: UserInfo) { }
public isStaff(): boolean {
return this.user.isAdmin || this.user.isModerator;
}
}
let user: User = new User({ id: "wqe" });
console.log(user.isStaff());
You can also make getters and setters for the properties, so you don't lose expresiveness.
class User {
constructor(private user: UserInfo) { }
public isStaff(): boolean {
return this.user.isAdmin || this.user.isModerator;
}
public get isAdmin() {
return this.user.isAdmin;
}
public set isAdmin(value) {
this.user.isAdmin = value;
}
}
You can then get or set isAdmin as you would on a normal object.
user.isAdmin = false;
You can also enforce that isAdmin can not be set by not making a setter for it. so the User class is immutable.
Related
I am working on a Node Js (TypeScript) architecture and for some reason, I want to bind my interface to a specific object. I am making a general class that is extended by other subclasses and it will have a very general code. So my code looks like
interface User {
name: string;
}
interface Profile {
title: string;
}
class Parent {
name: string;
interface: Interface; // Help required here, getting error can't use type as a variable
constructor( name, interface ) {
// Load schema and store here
this.name = name
this.interface = interface
}
// Though this is not correct I hope you get the idea of what I am trying to do
get (): this.interface {
// fetch the data and return
return data
}
set (data: this.interface): void {
// adding new data
}
}
class UserSchema extends Parent {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent {
// Class with custom functions for ProfileSchema
}
// Config file that saves the configs for different modules
const moduleConfig = [
{
name: "User Module",
class: UserSchema,
interface: User
},
{
name: "Profile Module",
class: ProfileSchema,
interface: Profile
},
]
const allModules = {}
// Loading the modules
moduleConfig.map(config => {
allModules[config.name] = new config.class(
config.name,
config.interface
)
})
export allModules;
I need suggestions on how should I bind my interfaces with their respective configs. Till now I have had no luck with that.
PS: All this code is separated into their respective files.
This is the use case for generics. You can even see them as "variable for types".
Instead of having an interface property in your Parent class, the latter would have a generic type:
class Parent<T> { // T is the generic type
name: string;
// interface: Interface; // generic is already provided at class level
constructor( name ) {
// Load schema and store here
this.name = name
}
get (): T {
// fetch the data and return
return data
}
set (data: T): void {
// adding new data
}
}
// Here you specify the concrete generic type
class UserSchema extends Parent<User> {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent<Profile> {
// Class with custom functions for ProfileSchema
}
I'm using this library in a TypeScript project.
And this is how my class looks like:
import OnvifManager from 'onvif-nvt'
Class OnvifApi {
// device: any = undefined
device = {} as OnvifDevice
constructor (...params) {
// definition
}
connect (): Promise<any> {
OnvifManager.connect(...params).then((response: OnvifDevice) => {
this.device = response
resolve(response)
}
}
coreService(): Promise<Type[]> {
this.device.add('core')
}
}
And this this interface I created for the response from onvif-nvt
interface OnvifDevice {
address: string
// type
// type
// type
add(name: string): void
}
This class is always giving an error in coreService saying that
this.device.add is not a function.
EDIT: This is the return of the connect method, which returns the whole Camera object.
Things are working when device is defined to any.
How can I do this interface mapping from JavaScript to TypeScript?
The reason is, in the interface, OnvifDevice, add is a mandatory parameter, but the device object is initialised with an empty object.
The dynamic mapping at this.device = response is not happening.
Also please check if the response has add method or not while you debug.
These are the two possible symptoms I see.
Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx
I'm developping a single app and at the moment the only good behavior is that I'm getting an user from an API with HttpClient method.
The method is store in a service.
Getting the user is a success but now I want to get a specific array from that user to re-use it by my will.
Should I make another service since this value will be use in 2 components ?
How should I procced to get this array in a var ?
Exemple of user object :
{
firstName: '',
lastName: '',
arrayIWant: []
}
My user is in a subject and here is the way I use it in a component
user: User;
userSubscription: Subscription;
constructor(
public userService: UserService
) {
}
ngOnInit() {
this.userSubscription = this.userService.userSubject.subscribe(
(user: User) => {
this.user = user;
}
);
this.userService.getSingleUserFromServer();
this.userService.emitUser();
}
ngOnDestroy() {
this.userSubscription.unsubscribe();
}
Should I put this code in every component where I want to use the user or is there a way to definie globaly the user ?
You can use a BehaviourSubject which will hold the last value of whatever that service populates the userSubject with
public userSubject: BehaviourSubject<User> = new BehaviourSubject(null);
getSingleUserFromServer(): void {
//get your user from http
userSubject.next(result);
}
In you HTML you can use the async pipe to display the values of the inner array you want. Or just use it in your component by subscribing to the last emission of the behaviourSubject
//X.Component
public subscriptionKiller: Subject<void> = new Subject();
ngOnInit(): void {
this.userService.userSubject
.pipe(takeUntil(this.subscriptionKiller))
.subscribe((lastUser: User) => {
someMethod(this.userService.userSubject.value.arrayIWant);
}
}
ngOnDestroy(): void {
this.subscriptionKiller.next()
}
I have this Customer class:
export class Customer {
id: number;
company: string;
firstName: string;
lastName: string;
name(): string {
if (this.company)
return this.company;
if (this.lastName && this.firstName)
return this.lastName + ", " + this.firstName;
if (this.lastName)
return this.lastName;
if (this.firstName)
return this.firstName;
if (this.id > 0)
return "#" + this.id;
return "New Customer";
}
}
In my controller I pull down a list of customers:
export class CustomersController {
static $inject = ["customerService", "workflowService"];
ready: boolean;
customers: Array<Customer>;
constructor(customerService: CustomerService, workflowService: WorkflowService) {
customerService.getAll().then(
(response) => {
this.customers = response.data;
this.ready = true;
},
() => {
this.ready = true;
}
);
workflowService.uiCustomer.reset();
}
}
angular.module("app")
.controller("CustomersController", ["customerService", "workflowService", CustomersController]);
If it helps, getAll() looks like this:
getAll(): ng.IHttpPromise<Array<Customer>> {
return this.http.get("/api/customers");
}
It's this statement that's causing me grief: this.customers = response.data;
But response.data is strongly typed, so shouldn't it "know" about Customer and name()?
When I do that, of course I am overwriting my strongly typed array with the dumb JSON one, which doesn't have my name() method on it.
So how do I keep my name method without copying every property of every object in the list?
Is this bad design on my part? Having these read-only properties was really common in C#, but I'm a little new to the javascript world. Should I be using a utility class instead?
My current work-around:
this.customers = response.data.map(customer => {
return angular.copy(customer, new Customer());
});
Feels wrong to build a whole new array and copy all those fields (in my real project Customer has many more properties).
Edit: I've found a few related SO questions, such as Mapping JSON Objects to Javascript Objects as mentioned by #xmojmr. My question was specific to TypeScript and I was wondering if TypeScript had any facilities of its own that would generate the javascript to make this a non-issue. If that's not the case, and we're sure TypeScript doesn't aim to solve this class of problem, then we can regard this question as a duplicate.
You're exactly right about what is happening. Typing in typescript mainly provides you with compiler checking. Under the covers, everything compiles to JavaScript which isn't strongly typed.
So, when you say:
getAll(): ng.IHttpPromise<Array<Customer>> {
return this.http.get("/api/customers");
}
all you are really doing is telling is telling the compiler "Hey, I'm pretty sure my api endpoint is going to return an array of Customer objects." But as you know, it really just returns a "dumb JSON" array.
What you could consider doing, is creating an interface that describes the JSON object being returned by the API endpoint. Something like:
interface ICustomer {
id: number;
company: string;
firstName: string;
lastName: string;
}
And then getAll() becomes:
getAll(): ng.IHttpPromise<Array<ICustomer>> {
return this.http.get("/api/customers");
}
Then you could have a class who's constructor takes ICustomer as a parameter. Or you could create a class with a static method that takes ICustomer and returns the "name".
Obviously, what you are doing now works, but I think you're right to be looking for something that better communicates the intent.