I ran into a flow error while doing essentially the basic redux-reducer example from flow docs.
Error from flow added to the code here: on the REMOVE switch case: action is not resolved to the correct type.
If i hover over payload in vscode, in the ADD case it displays it as AddAction, but on REMOVE case it is displayed as a union of all the actions, i.e. Action.
What am i missing or understanding wrong? Flow should deduct the correct type down from the Actions union to the only possible type(s) inside if and switch.
// #flow
const initialState = [];
type Item = { id: number, data: string };
type State = Item[];
type AddAction = {
type: 'ADD',
payload: Item
};
type RemoveAction = {
type: 'REMOVE',
payload: { id: number }
};
type ClearAction = {
type: 'CLEAR'
};
type Action = AddAction | RemoveAction | ClearAction;
const reducer: (State, Action) => State = (state = initialState, action) => {
switch (action.type) {
case 'ADD': {
return [...state, action.payload];
}
case 'REMOVE': {
return state.filter(t => t.id !== action.payload.id);
^ property `payload`. Property not found in
}
case 'CLEAR': {
return [];
}
default:
(action: empty);
return state;
}
};
export default reducer;
code on try flow
another try-flow repl where i do essentially the same thing and type inferring works as expected
Ok, it seems that the problem was the use of action inside Array.filter arrow function:
If i replace the REMOVE case contents with
case 'REMOVE': {
const id = action.payload.id;
return state.filter(t => t.id !== id);
}
the errors go away.
I guess flow cannot infer the type inside the arrow function. Would be interesting to know why.
edit: related question
So, flow invalidates the union refinement because it assumes filter() might be doing side effects to reducer parameter action (docs). Storing the action or payload in a const before using fixes this.
Related
I have this sample TypeScript code that is supposed to deserialize a simple JSON into an instance of class Person and then call foo method on it, but it doesn't work:
class Person {
name!: string;
age!: number;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
The output of console show that object is in a proper shape, but it is not recognized as Person:
Object { name: "Peter", age: 44 }
age: 44
name: "Peter"
Thus when it tries to call foo method it fails:
Uncaught (in promise) TypeError: data.foo is not a function
http://127.0.0.1:8000/app.js:14
promise callback* http://127.0.0.1:8000/app.js:12
How can I fix it? Should I use Object.assign or there is another better/native solution?
let x = (<any>Object).assign(Object.create(Person.prototype), data);
x.foo();
Remember, TypeScript is just a way of annotating JavaScript code with type guards. It doesn't do anything extra. For example, saying that the object returned by response.json() should be treated as a Promise<Person> does not mean it will invoke the constructor of your Person class. Rather, you'll just be left with a plain old JavaScript object that has a name and an age.
It looks to me like you'll need to create a constructor for your Person class which can create a new instance of a Person based on an object that matches its interface. Something like this, perhaps?
interface PersonLike {
name: string;
age: string;
}
class Person implements PersonLike {
constructor(data: PersonLike) {
this.name = data.name;
this.age = data.age;
}
name: string;
age: string;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<PersonLike>;
}).then((data) => {
const person = new Person(data);
person.foo();
});
I'd also recommend using a type guard instead of the as keyword, in case the API you're fetching data from changes. Something like this, perhaps:
function isPersonLike(data: any): data is PersonLike {
return typeof data?.name === 'string' && data?.age === 'string';
}
fetch("/api/data")
.then(response => {
return response.json();
}).then((data: unknown) => {
if (isPersonLike(data)) {
const person = new Person(data);
person.foo();
}
});
... is supposed to deserialize a simple JSON into an instance of class Person and then ...
Unfortunately, generic type in TypeScript only works as some kind of model design assistant. It will never be compiled into JavaScript file. Take your "fetch" code for example:
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
After compile the above TypeScript file into JavaScript, we can find the code as Promise<Person> is completely removed:
fetch("/api/data")
.then(function (response) {
return response.json();
}).then(function (data) {
console.log(data);
data.foo();
});
To implement "type safe deserialization", you need to save class/prototype information during serialization. Otherwise, these class/prototype information will be lost.
... or there is another better/native solution? ... BTW, what if a class field has a custom type, so it is an instance of another class?
No, there is no native solution, but you can implement "type safe" serialization/deserialization with some libraries.
I've made an npm module named esserializer to solve this problem automatically: save JavaScript class instance values during serialization, in plain JSON format, together with its class name information. Later on, during the deserialization stage (possibly in another process or on another machine), esserializer can recursively deserialize object instance, with all Class/Property/Method information retained, using the same class definition. For your "fetch" code case, it would look like:
// Node.js server side, serialization happens here.
const ESSerializer = require('esserializer');
router.get('/api/data', (req, res) => {
// ...
res.json(ESSerializer.serialize(anInstanceOfPerson));
});
// Client side, deserialization happens here.
const ESSerializer = require('esserializer');
fetch("/api/data")
.then(response => {
return response.text() as Promise<string>;
}).then((data) => {
const person = ESSerializer.deserialize(data, [Person, CustomType1, CustomType2]);
console.log(person);
person.foo();
});
I need to set the displayName variable but I have no idea how to get to it. For context, I'm making a C# application to set this variable to something else. The parent variables to displayName vary depending on the user that is using this application.
I have blurred these as to not reveal any of my personal information.
I think I might need to loop through JSON object children, but I'm not sure.
Hey so you are correct you are going to have to iterate over the object and search through to find display name.
I wrote a little function below that will recursively go through the object and search for displayName. Obviously its hard if you never know the location or the pathname so you have to have a pretty open way to search the JSON object.
If you can control the way you are requesting the data maybe you could change the format so the data structure is more consistent, but I don't really know anything about where your getting the data from.
This is just one of the many ways to do it.
const obj = {
authenticationDatabase : {
accessToken: 'Mock',
profiles: {
displayName: 'THIS IS A MOCK USER NAME'
},
properties: [],
username: 'MOCK'
}
}
const obj2 = {
authenticationDatabase : {
accessToken: 'Mock',
profiles: {
deep: {
nested: {
object: {
displayName: 'THIS IS A MOCK USER NAME'
}
}
}
},
properties: [],
username: 'MOCK'
}
}
const findDisplayName = obj => {
if(!obj || typeof(obj) != 'object'){
return false;
}
if(Object.keys(obj).includes("displayName")){
return obj["displayName"]
}
for(const key in obj){
if(findDisplayName(obj[key])){
return findDisplayName(obj[key])
}
}
return false;
}
console.log(findDisplayName(obj))
console.log(findDisplayName(obj2))
So I'm getting the following JSON structure from my asp.net core api:
{
"contentType": null,
"serializerSettings": null,
"statusCode": null,
"value": {
"productName": "Test",
"shortDescription": "Test 123",
"imageUri": "https://bla.com/bla",
"productCode": null,
"continuationToken": null
}
}
I have the following typescript function that invokes the API to get the above response:
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; //not working...
console.log("DATA: " + data);
console.log("DATA: " + data['value']);
return data;
});
}
ProductVM:
export interface ProductVM {
productName: string;
shortDescription: string;
imageUri: string;
productCode: string;
continuationToken: string;
}
My problem is that I can't deserialize it to ProductVM. The console logs just produce [object Object]
How can I actually map the contents of the value in my json response to a ProductVM object?
Is it wrong to say that data is a ProductVM in the map function? I have tried lots of different combinations but I cannot get it to work!
I'm unsure whether I can somehow automatically tell angular to map the value array in the json response to a ProductVM object or if I should provide a constructor to the ProductVM class (it's an interface right now), and extract the specific values in the json manually?
The data object in the map method chained to http is considered a Object typed object. This type does not have the value member that you need to access and therefore, the type checker is not happy with it.
Objects that are typed (that are not any) can only be assigned to untyped objects or objects of the exact same type. Here, your data is of type Object and cannot be assigned to another object of type ProductVM.
One solution to bypass type checking is to cast your data object to a any untyped object. This will allow access to any method or member just like plain old Javascript.
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: any) => this.externalProduct = data.value);
}
Another solution is to change your API so that data can deliver its content with data.json(). That way, you won't have to bypass type checking since the json() method returns an untyped value.
Be carefull though as your any object wil not have methods of the ProductVM if you ever add them in the future. You will need to manually create an instance with new ProductVM() and Object.assign on it to gain access to the methods.
From angular documentation: Typechecking http response
You have to set the type of returned data when using new httpClient ( since angular 4.3 ) => this.http.get<ProductVM>(...
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get<ProductVM>("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; // should be allowed by typescript now
return data;
});
}
thus typescript should leave you in peace
Have you tried to replace
this.externalProduct = data;
with
this.externalProduct = data.json();
Hope it helps
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map(data => {
this.externalProduct = <ProductVM>data;
console.log("DATA: " + this.externalProduct);
return data;
});
}
So, first we convert the response into a JSON.
I store it into response just to make it cleaner. Then, we have to navigate to value, because in your data value is the object that corresponds to ProductVM.
I would do it like this though:
Service
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get(`api/product?productCode=${code}`)
.map(data => <ProductVM>data)
.catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
Component
this.subscription = this.myService.getProductExternal(code).subscribe(
product => this.externalProduct = product,
error => console.warn(error)
);
I used this approach in a client which uses the method
HttpClient.get<GENERIC>(...).
Now it is working. Anyway, I do not understand, why I do not receive a type of T back from the http client, if I don't use the solution provided in the answer above.
Here is the client:
// get
get<T>(url: string, params?: [{key: string, value: string}]): Observable<T> {
var requestParams = new HttpParams()
if (params != undefined) {
for (var kvp of params) {
params.push(kvp);
}
}
return this.httpClient.get<T>(url, {
observe: 'body',
headers: this.authHeaders,
params: requestParams
}).pipe(
map(
res => <T>res
)
);
}
When using a generic modal or toast with a confirm button, it becomes useful to be able to pass an action into this component so it can be dispatched when you click confirm.
The action may look something like this:
export function showConfirm({modalConfirm}) {
return {
type: 'MODALS/SHOW_MODAL',
payload: {
modalId: getUuid(),
modalType: 'CONFIRM',
modalConfirm : modalConfirm,
},
};
}
Where modalConfirm is another action object such as:
const modalConfirm = {
type: 'MAKE_SOME_CHANGES_AFTER_CONFIRM',
payload: {}
}
The modalConfirm action is dispatched inside the modal component using dispatch(modalConfirm) or even dispatch(Object.assign({}, modalConfirm, someResultFromTheModal)
Unfortunatley this solution only works if modalConfirm is a simple redux action object. This system is clearly very limited. Is there anyway you can pass a function (such as a thunk) in instead of a simple object?
Ideally, something full featured likes this:
const modalConfirm = (someResultFromTheModal) => {
return (dispatch, getState){
dispatch({
type: 'MAKE_SOME_UPDATES',
payload: someResultFromTheModal
})
dispatch({
type: 'SAVE_SOME_STUFF',
payload: http({
method: 'POST',
url: 'api/v1/save',
data: getState().stuffToSave
})
})
}
}
Funny, putting an action object in the store and passing it as a prop to a generic dialog is exactly the approach I came up with myself. I've actually got a blog post waiting to be published describing that idea.
The answer to your question is "Yes, but....". Per the Redux FAQ at http://redux.js.org/docs/FAQ.html#organizing-state-non-serializable , it's entirely possible to put non-serializable values such as functions into your actions and the store. However, that generally causes time-travel debugging to not work as expected. If that's not a concern for you, then go right ahead.
Another option would be to break your modal confirmation into two parts. Have the initial modal confirmation still be a plain action object, but use a middleware to watch for that being dispatched, and do the additional work from there. This is a good use case for Redux-Saga.
I ended up using string aliases to an actions library that centrally registers the actions.
Modal emmiter action contains an object with functionAlias and functionInputs
export function confirmDeleteProject({projectId}) {
return ModalActions.showConfirm({
message: 'Deleting a project it permanent. You will not be able to undo this.',
modalConfirm: {
functionAlias: 'ProjectActions.deleteProject',
functionInputs: { projectId }
}
})
}
Where 'ProjectActions.deleteProject' is the alias for any type of complicated action such as:
export function deleteProject({projectId}) {
return (dispatch)=>{
dispatch({
type: 'PROJECTS/DELETE_PROJECT',
payload: http({
method: 'DELETE',
url: `http://localhost:3000/api/v1/projects/${projectId}`,
}).then((response)=>{
dispatch(push(`/`))
}),
meta: {
projectId
}
});
}
}
The functions are registered in a library module as follows:
import * as ProjectActions from '../../actions/projects.js';
const library = {
ProjectActions: ProjectActions,
}
export const addModule = (moduleName, functions) => {
library[moduleName] = functions
}
export const getFunction = (path) => {
const [moduleName, functionName] = path.split('.');
// We are getting the module only
if(!functionName){
if(library[moduleName]){
return library[moduleName]
}
else{
console.error(`Module: ${moduleName} could not be found.`);
}
}
// We are getting a function
else{
if(library[moduleName] && library[moduleName][functionName]){
return library[moduleName][functionName]
}
else{
console.error(`Function: ${moduleName}.${functionName} could not be found.`);
}
}
}
The modalConfirm object is passed in to the modal by props. The modal component requires the getFunction function in the module above. The modalConfirm object is transformed into a function as follows:
const modalConfirmFunction = (extendObject, modalConfirm) => {
const functionFromAlias = getFunction(modalConfirm.functionAlias);
if(functionFromAlias){
dispatch(functionFromAlias(Object.assign({}, modalConfirm.functionInputs, extendObject)));
}
}
As you can see, this function can take in inputs from the modal. It can execute any type of complicated action or thunk. This system does not break time-travel but the centralized library is a bit of a drawback.
In an effort to properly instantiate Typescript objects from data received over HTTP as JSON, I was exploring the possibility of using the for..in loop coupled with .hasOwnProperty() like so:
class User {
private name: string;
private age: number;
constructor(data: JSON) {
console.log('on constructor\ndata', data);
for (var key in data) {
console.log('key:', key);
if (User.hasOwnProperty(key)) {
console.log('User has key:', key);
this[key] = data[key];
}
}
}
displayInfo(): string{
return JSON.stringify(this);
}
}
let button = document.createElement('button');
button.textContent = "Test";
button.onclick = () => {
try{
let json = JSON.parse('{"name": "Zorro","age": "24"}');
let usr = new User(json);
console.log(usr.displayInfo());
}catch (error){
alert(error);
}
}
document.body.appendChild(button);
Using similar code in my project fails completely. That is expected as the compiled JS code has no awareness of the private TS vars and so, hasOwnProperty is always false.
However, I was using the Typescript Playground, and running that code there produces the following output in the console:
on constructor
data Object {name: "Zorro", age: "24"}
key: name
User has key: name
key: age
{"name":"Zorro"}
As you can see, there are clearly some unexpected things happening here. The first key is recognized and the new User instance is initialized with the value from the JSON, yet that does not happen for the second key.
Can someone explain why this happens?
As was pointed out in the comments, you should be using this.hasOwnProperty instead of User.hasOwnProperty. And as you noticed, this code is busted anyway because the property declarations in the class don't actually create own properties on the object (they would need to be initialized for this to happen).
But why did you get a hit on the name key? The User object is a constructor function for your class. Functions do have a name property:
function fn() { }
console.log(fn.name); // prints 'fn'
They don't have an age property, of course.
Your constructor would of course just have to look like this, if you want to construct User instances from plain JavaScript objects:
constructor(data: any) {
this.name = data.name;
this.age = data.age;
}