Function taking a function returning a generic type is not typesafe - typescript-generics

I experience this when using React hooks, but it is a general TypeScript question.
You can see it in the playground
When I use the generic parameter which I finds most intuitive the function I pass in isn't completely typesafe. As long as it returns an object with the props in T the compiler is happy, but I can also add props that are not part of T. This becomes a problem when T has some optional props, because I can miss spell them and not know it.
If I instead set the return type explicit on the function I pass in, everything works as expected, but this is not an intuitive usage when the function I call have a generic parameter.
Can someone please explain why the compiler is allowing this?
const foo = <T>(f: ()=>T) => {
return f();
};
type Result = {
bar: number;
foo?:number;
}
const strange = foo<Result>(() => {
return {
bar: 42,
baz: 12, // why is this prop allowd?
FOO: 13 // ups I might think I set the foo prop, but I spelled it wrong
};
});
const expected = foo((): Result => {
return {
bar:42,
FOO: 12 // this is not allowd, which is what I wxpected
};
});

Excess property check is triggered only for "fresh" object literal.
The freshness of an object literal is lost in case of type assertion and type widening.
in strange function, there is a type widening on the return type (see issue #241). So excess properties are valid.
const strange = foo<Result>(() => {
// type widening occurs here
// freshness is lost
// No excess property check triggered
return {
bar: 42,
baz: 12, // excess property allowed
FOO: 13 // excess property allowed
};
});
in the expected function, there is no type widening since the return type is explicitly specified. the freshness of the object literal is maintained, and the excess property check is triggered
const expected = foo((): Result => {
// No type assertion
// object literal is fresh
// Excess property check is triggered
return {
bar:42,
FOO: 12 // this is not allowd, which is what I wxpected
};
});

Related

Flow property is missing in mixed passed - despite the type annotation

I have the following in my declarations file (included in my [libs]):
export type EtlFieldNoIdxT = {
name: Name,
purpose: Purpose,
}
export type EtlFieldT = {
idx: number,
...EtlFieldNoIdxT
}
And the following in my use of the types:
export const createEtlField = (
etlFields: { [Name]: EtlFieldT },
newField: EtlFieldNoIdxT,
) => {
if (etlFields === {}) {
throw new Error({
message: 'Cannot create a new etlField with an empty etlFields',
});
}
const field: EtlFieldT = {
idx: maxId(etlFields, 'idx') + 1,
...newField,
};
const subject: Name = Object.values(etlFields).find(
(f) => f.purpose === 'subject', // <<< f.purpose "missing in mixed" error
).name; // <<< .name "missing in mixed" error
return newEtlField(field, subject);
};
Despite having annotated the input, can flow not infer the type of what Object.values would thus return?
Thank you in advance for pointing out my misunderstanding.
- E
If you check the declaration for Object.values you'll find that it returns an array of mixed:
static values(object: $NotNullOrVoid): Array<mixed>;
A quick google search came back with
https://davidwalsh.name/flow-object-values
So to solve your issue, you wrap Object.values(...) with any, and then inside your find arg you can type it as EtlFieldT and finally refine your type back to EtlFieldT after find.
const subject: Name = ((Object.values(etlFields): any).find(
(f: EtlFieldT) => f.purpose === 'subject',
): EtlFieldT).name;
Though you should be aware that find has the possibility of returning undefined. So to be sound, you should run the find, and declare subject if the value exists.

Flow errors when dealing with nullable types

I have working on a redux reducer with the following state:
export type WishlistState = {
+deals: ?DealCollection,
+previousWishlist: ?(Deal[]),
+currentWishlist: ?(Deal[]),
+error: ?string
};
export type DealCollection = { [number]: Deal };
export const initialState: WishlistState = {
deals: null,
previousWishlist: null,
currentWishlist: null,
error: null
};
export default function wishlistReducer(
state: WishlistState = initialState,
action: WishlistAction
): WishlistState {
switch (action.type) {
case "GET_DEALS_SUCCESS":
return { ...state, deals: action.deals };
case types.GET_WISHLIST_SUCCESS:
console.log(action);
const currentWishlist: Deal[] = action.wishlistIds.map(
// ATTENTION: THIS LINE HERE
d => state.deals[d]
);
return {
...state,
currentWishlist,
previousWishlist: null,
error: null
};
// ...other cases
default:
return state;
}
}
The line I've flagged with the comment is getting a flow error on the d in the
brackets:
Cannot get `state.deals[d]` because an index signature declaring the expected key/value type is missing in null or undefined.
This is happening because of the type annotation: deals: ?DealCollection, which is made clearer if I change the line to this:
d => state.deals && state.deals[d]
Which moves the error to state.deals; and the idea is that if state.deals is null, then the callback returns null (or undefined), which is not a acceptable return type for a map callback.
I tried this and I really thought it would work:
const currentWishlist: Deal[] = !state.deals
? []
: action.wishlistIds.map(d => state.deals[d]);
It would return something acceptable if there are no deals is null, and never get to the map call. But this puts the error back on the [d] about the index signature.
Is there any way to make Flow happy in this situation?
Flow invalidates type refinements whenever a variable may have been modified. In your case, the thought of checking !state.deals is a good start; however, Flow will invalidate the fact that state.deals must have been a DealCollection because (theoretically) you could be modifying it in your map function. See https://stackoverflow.com/a/43076553/11308639 for more information on Flow type invalidation.
In your case, you can "cache" state.deals when you have refined it as a DealCollection. For example,
type Deal = string; // can be whatever
type DealCollection = { [number]: Deal };
declare var deals: ?DealCollection; // analogous to state.deals
declare var wishlistIds: number[]; // analogous to action.wishlistIds
let currentWishlist: Deal[] = [];
if (deals !== undefined && deals !== null) {
const deals_: DealCollection = deals;
currentWishlist = wishlistIds.map(d => deals_[d]);
}
Try Flow
that way you can access deals_ without Flow invalidating the refinement.

Flow Type + Redux: Type for action that won't conflict with actions for other reducers

I have a reducer for storing preferences. It has two action types. One for loading in all preferences from database and another for updating a single preference. I have a working standalone example but it breaks once used inside of my app.
The issue is that my preferences reducer only handles two types of actions, while my app has multiple reducers that fire other actions. A solution to get the code running is to add a third general type for actions not related to this reducer. That however creates Property not found in 'object type'. errors when I try to access properties of the action.
Working flow example
// #flow
const LOAD_PREFS_SUCCESS = 'LOAD_PREFS_SUCCESS';
const UPDATE_PREF = 'UPDATE_PREF';
type aType = {
+type: string
};
export type actionType = {
+type: typeof LOAD_PREFS_SUCCESS,
prefs: Array<{_id: string, value: any}>
} | {
+type: typeof UPDATE_PREF,
id: string,
value: any
};
export default (state: {} = {}, action: actionType) => {
if (action.type === LOAD_PREFS_SUCCESS) {
action.prefs.forEach(p => {
console.log(p);
});
}
switch (action.type) {
case LOAD_PREFS_SUCCESS: {
const newState = {};
action.prefs.forEach(p => {
newState[p._id] = p.value;
});
return newState;
}
case UPDATE_PREF: {
return { ...state, [action.id]: action.value };
}
default:
return state;
}
};
This is valid flow but when the app actually runs, I get an error when an action with type INIT_APP or something runs. The error says action must be one of: and then it lists the two types I have in actionType as the expected and an actual of { type: string }.
I can get the app running by adding a third type to actionType like this:
export type actionType = {
+type: typeof LOAD_PREFS_SUCCESS,
prefs: Array<{_id: string, value: any}>
} | {
+type: typeof UPDATE_PREF,
id: string,
value: any
} | {
+type: string
};
Even though the app now runs without error, it does not pass flow type check. Throwing errors of Property not found in object type. Here is an example on flow.org
Since every reducer ends up seeing every action, you'll want the type of this reducer function to include all the possible actions in your app. I usually define a single variant actionType with everything available in the app and use that in every reducer.
The reason why your last code example doesn't work is because the third, anonymous action type {type: string} is too vague. Before this, Flow could look at the two options in the action, and see that it would know which one was which based on the case statements. But with the third action type, an action like {type: "LOAD_PREFS_SUCCESS"} would match the third case in the type. So testing action.type === LOAD_PREFS_SUCCESS is no longer enough to prove that the action will have a prefs key.
So there are two ways to fix this:
If you change your action type to be more specific and include all the specific action types, your reducer should go back to type-checking.
Otherwise, add a dummy case, like | {type: "NOT-REAL"} so that Flow forces your reducer to have a default case for actions it doesn't understand.

Strange type empty Flow syntax inside default statement of Redux reducer

I found the code sample while searching for ways to use flow with redux here:
https://flow.org/en/docs/frameworks/redux/
Peculiar syntax is (action: empty); Is it just a bit of flow magic intended to be used just inside default case of switch statement or does it have other uses?
It looks like out of place function type statement without return value type but with parameter of strange type 'empty', which I couldn't find documentation about.
// #flow
type State = { +value: boolean };
type FooAction = { type: "FOO", foo: boolean };
type BarAction = { type: "BAR", bar: boolean };
type Action = FooAction | BarAction;
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FOO": return { ...state, value: action.foo };
case "BAR": return { ...state, value: action.bar };
default:
(action: empty);
return state;
}
}
empty is Flow's bottom type. I believe the main motivation for its initial introduction was symmetry but it has proven to have some uses. As you have identified it can be used in this case to make Flow enforce exhaustiveness. It can be used similarly in a chain of if/else statements.
However, it can be used anytime when you want Flow to prevent any actual value from ending up somewhere. This is very vague, so here are a couple examples:
// Error: empty is incompatble with implicitly-returned undefined
function foo(): empty {
}
// No error since the function return is not reached
function foo2(): empty {
throw new Error('');
}
function bar(x: empty): void {
}
// Error: too few arguments
bar();
// Error: undefined is incompatible with empty
bar(undefined);
In the foo examples, we can see that Flow enforces that a return is never reached in a function returning empty. In the bar example, we can see that Flow prevents the function from being called.

React-redux project - chained dependent async calls not working with redux-promise middleware?

I'm new to using redux, and I'm trying to set up redux-promise as middleware. I have this case I can't seem to get to work (things work for me when I'm just trying to do one async call without chaining)
Say I have two API calls:
1) getItem(someId) -> {attr1: something, attr2: something, tagIds: [...]}
2) getTags() -> [{someTagObject1}, {someTagObject2}]
I need to call the first one, and get an item, then get all the tags, and then return an object that contains both the item and the tags relating to that item.
Right now, my action creator is like this:
export function fetchTagsForItem(id = null, params = new Map()) {
return {
type: FETCH_ITEM_INFO,
payload: getItem(...) // some axios call
.then(item => getTags() // gets all tags
.then(tags => toItemDetails(tags.data, item.data)))
}
}
I have a console.log in toItemDetails, and I can see that when the calls are completed, we eventually get into toItemDetails and result in the right information. However, it looks like we're getting to the reducer before the calls are completed, and I'm just getting an undefined payload from the reducer (and it doesn't try again). The reducer is just trying to return action.payload for this case.
I know the chained calls aren't great, but I'd at least like to see it working. Is this something that can be done with just redux-promise? If not, any examples of how to get this functioning would be greatly appreciated!
I filled in your missing code with placeholder functions and it worked for me - my payload ended up containing a promise which resolved to the return value of toItemDetails. So maybe it's something in the code you haven't included here.
function getItem(id) {
return Promise.resolve({
attr1: 'hello',
data: 'data inside item',
tagIds: [1, 3, 5]
});
}
function getTags(tagIds) {
return Promise.resolve({ data: 'abc' });
}
function toItemDetails(tagData, itemData) {
return { itemDetails: { tagData, itemData } };
}
function fetchTagsForItem(id = null) {
let itemFromAxios;
return {
type: 'FETCH_ITEM_INFO',
payload: getItem(id)
.then(item => {
itemFromAxios = item;
return getTags(item.tagIds);
})
.then(tags => toItemDetails(tags.data, itemFromAxios.data))
};
}
const action = fetchTagsForItem(1);
action.payload.then(result => {
console.log(`result: ${JSON.stringify(result)}`);
});
Output:
result: {"itemDetails":{"tagData":"abc","itemData":"data inside item"}}
In order to access item in the second step, you'll need to store it in a variable that is declared in the function scope of fetchTagsForItem, because the two .thens are essentially siblings: both can access the enclosing scope, but the second call to .then won't have access to vars declared in the first one.
Separation of concerns
The code that creates the action you send to Redux is also making multiple Axios calls and massaging the returned data. This makes it more complicated to read and understand, and will make it harder to do things like handle errors in your Axios calls. I suggest splitting things up. One option:
Put any code that calls Axios in its own function
Set payload to the return value of that function.
Move that function, and all other funcs that call Axios, into a separate file (or set of files). That file becomes your API client.
This would look something like:
// apiclient.js
const BASE_URL = 'https://yourapiserver.com/';
const makeUrl = (relativeUrl) => BASE_URL + relativeUrl;
function getItemById(id) {
return axios.get(makeUrl(GET_ITEM_URL) + id);
}
function fetchTagsForItemWithId(id) {
...
}
// Other client calls and helper funcs here
export default {
fetchTagsForItemWithId
};
Your actions file:
// items-actions.js
import ApiClient from './api-client';
function fetchItemTags(id) {
const itemInfoPromise = ApiClient.fetchTagsForItemWithId(id);
return {
type: 'FETCH_ITEM_INFO',
payload: itemInfoPromise
};
}

Resources