Flowtype constantly requiring null checks - flowtype

I'm wondering how to avoid these numerous null checks or at least understand what the point is because it seems counter-productive.
Flowtype is giving me an error for this if I omit the null check:
var myEl = new MyElement()
if (document.body != null) { // error on next line if omitted
document.body.appendChild(myEl)
}
I have to do that null check for the document body in every single callback too, because who knows, maybe the body is null here right?!
I think this is total overkill. Not only that, but what's the point of such a simple nullcheck? It will just silently skip over a vital part of the program and exhibit undefined behavior somewhere else and make debugging the app that much harder.
I'd really prefer just having a null exception at this point if an error ever happens here, because to be really sure this tiny 2-line code segment that I'd write in javascript would have to be like this in flowtype:
var myEl = new MyElement()
if (document.body != null) {
document.body.appendChild(myEl)
} else {
console.error("null error")
}
So 4 additional code lines and some nesting just to trace something I'd get for free if I just let the app run into an error. And I need those 4 lines on every single querySelector. On every single document.body. On every single getElementByTagName. This alone probably increases my entire codebase by 10%.
What's the point of enforcing this so strictly?
In other languages I'd also be able to try-catch around these hotspots gradually as needed, flow doesn't let me do that either. It shows errors whether I add a try-catch or not.

By using a type checker, you are opting into the rules that it enforces. Accessing a property on a nullable type is one of those restrictions. So if you want to have exceptions for null values, you need to explicitly throw to prove to Flow that it is what you want. You could for instance make a module like
if (!document.body) throw new Error("Unexpectedly missing <body>.");
export const body: HTMLElement = document.body;
export function querySelector(el: HTMLElement, selector: string): HTMLElement {
const result = el.querySelector(selector);
if (!result) throw new Error(`Failed to match: ${selector}`);
return result;
}
By throwing, these functions explicitly say "I will return an element" in all cases, and in null cases, they will throw exceptions.
Then in your normal code, you are guaranteed you can use those
import {body, querySelector} from "./utils";
body.appendChild(document.createElement('div'));
querySelector(body, 'div').setAttribute('thing', 'value');
and it will typecheck property.

When I know for sure that my variable won't be null and Flow doesn't, I use an unwrap() function:
export default function unwrap<T>(value: T): $NonMaybeType<T> {
if (value !== null && value !== undefined) return value
throw new Error('Unwrapping not possible because the variable is null or undefined!')
}

Related

Redux saga: take every action where error is true

Is there a possibility to specify whether the action has its error field set to true?
const response = function*() {
yield takeEvery("CLIENT_RESPONSE", handleResponse);
}
However, we don't know whether the action with type CLIENT_RESPONSE has its error field set to true or not.
I know I can check this in the handleResponse but that seems to be more work than it should. For instance, the handleResponse might get complex because for both the non-error and error case I need to write a lot of code (i.e. I want to have different handlers for both cases).
So is there a way to specify to only take that action when error is set to true?
According to Saga API reference, the pattern (first argument) of takeEvery can be String, Array or Function.
You can achieve what you want by passing a function:
const response = function*() {
yield takeEvery(action => (action.type === "CLIENT_RESPONSE" && !action.error), handleResponse);
}

Why Flow still complains about null values for document.getElementById

Why even with an IF check, Flow still complains about a possibly null value
if(document && document.getElementById("myID") && document.getElementById("myID").offsetWidth){
console.log(document.getElementById("myID").offsetWidth);
}
Gives this error
^ property `offsetWidth`. Property cannot be accessed on possibly null value
Flow has no way to know that the success of first call to getElementById means that the later ones will also succeed. For all it knows, reading the offsetWidth property could cause getElementById to start returning null the next time it is called.
You'll need to store the value, e.g.
const myIdEl = document && document.getElementById("myID");
if(myIdEl && myIdEl.offsetWidth) {
console.log(myIdEl.offsetWidth);
}
this way there is no way for myIdEl to become null after it has been referenced.
For HTMLElement (and extensions of HTMLElement like VideoHTMLElement) in FlowType, I'd recommend using instanceof to validate the Type and to validate that it's not null.
Also, I don't believe you need to check if document exists, that is defined globally in flow (1)*
<HTMLElement> Example
const myIdEl: ?HTMLElement = document.getElementById('myID');
if (myIdEl instanceof HTMLElement) {
// continue
console.log(myIdEl.offsetWidth);
}
<HTMLSelectElement> Example
const selectEl: ?HTMLElement = document.getElementById('someSelectElement');
// Checks correct type (!null !undefined come for free)
if (selectEl instanceof HTMLSelectElement) {
const selectedVal = selectEl.options[selectEl.selectedIndex].value;
}
<HTMLVideoElement> Example using invariant
import invariant from 'invariant';
const videoContent = document.getElementById('video-player');
invariant(videoContent instanceof HTMLVideoElement, 'No video element');
// do stuff with video api
videoContent.volume = 0;
videoContent.plause();
https://github.com/facebook/flow/blob/f3f29f7fd8c5aa73ac5a8a546ccfbc29cd7505fe/lib/dom.js#L1288

Vuefire Firebase update issues

I'm having som issues with updating Firebase from VueFire. I m trying to use the following method, but it yells at me if I leave any field blank (which is supposed to happen often in setup) Any idea why this gets mad if .update with a blank field?
Error: Uncaught Error: Firebase.update failed: First argument contains undefined in property 'businesses.somebusiness.video'
updatePost(post) {
postsRef.child(post['.key']).update({
name: post.name,
video: post.video,
story: post.story,
cover: post.cover,
card: post.card
})
},
At one point I had the above re-written like so:
updatePost: function (post) {
const businesschildKey = post['.key'];
delete post['.key'];
/* Set the updated post value */
this.$firebaseRefs.posts.child(businesschildKey).set(post)
},
It worked amazingly but deleting the key seemed to cause weird ordering issues in Vue. I would prefer to stick with the top method if I can find a way to not have it trow an error if one is left blank.
According to this post,
When you pass an object to Firebase, the values of the properties can
be a value or null (in which case the property will be removed). They
can not be undefined, which is what you're passing in according to the
error.
Your error message suggests that post.video's value is undefined. You can use logical-or to provide a fallback value like so:
video: post.video || null,
That means whenever post.video has a false-y value, the expression will evaluate to null. That could catch empty string or numeric 0, though. To be more precisely correct, you should use
video: typeof post.video === 'undefined' ? null : post.video,
If you need to do this check for many values, you can write a function for it:
function nullIfUndefined(value) {
return typeof value === 'undefined' ? null : value;
}
then your expression would just be
video: nullIfUndefined(post.video),

Is it possible to safely use Switch over FlowType union types (String Enums)?

In the following example, since I'm using matching over type of Message using the switch statement, I would like flow to recognise my incorrect case of 'ENUM_TYPO'. It currently doesn't.
type Message = 'BROADCAST_MESSAGE' | 'PRIVATE_MESSAGE';
const message: Message = 'BROADCAST_MESSAGE';
switch (message) {
case 'ENUM_TYPO':
// Do Broadcast
break;
default:
break;
}
As of v0.32.0, Flow does not complain about unreachable code, unless it's something like
// #flow
function foo() {
throw new Error();
return 123; // This will error
}.
However, consider the following code
// #flow
function foo(x: string): Object {
if (x === 123) {
return x;
}
return {};
}
Will currently will not error on this code. Flow does in fact notice that x === 123 will never be true. Inside the if block, Flow will refine the type of x to the empty type, since it doesn't believe that this code will ever be reached. That is why it doesn't complain about the return x statement.
One of the members of the Flow team is almost done with adding reachability analysis to Flow. Once this improvement lands (I'm guessing v0.34.0?), Flow will complain when it sees a conditional that it thinks will always fail. This will help you with your example, since switch statement cases are basically strict equality checks.

#ngrx/store Ignore first emitted value

store.select() emits previous store state.
Is it possible to subscribe to changes from "this point forward" without getting the previous store value?
If you are not interested in the first emitted value, you should be able to use the skip operator:
store.select(...).skip(1)...
skip operators need piping now, you can use skip like this:
store.pipe(select(...), skip(1));
In terms of the 'hacky' part, it is a standard practice in ngrx to set an initial state with properties set to null. and that value gets emitted initially. so the first value you get will be null in these cases.
Alternatively you could also consider skipwhile(https://www.learnrxjs.io/learn-rxjs/operators/filtering/skipwhile) and use it like this:
store.pipe(select(...), skipWhile(val => val === undefined));
where undefined is the initial value of the property you are interested in. Rather than setting the initial value of the property to undefined, you could use null as the initial value as well, and change the above skipwhile() accordingly.
Just sharing my thoughts (and solution) after reading #Niz's answer.
This is a perfect, practical example of how to utilize the difference between null and undefined. When you initialize your state with null, you're basically saying:
I don't care about differentiating the nullable future state from the
initial one. I don't care if the user is null because he has signed
out or because he just didn't sign in
However, in some cases this could be insufficient. Think about a case when you need an asynchronous call (implemented in effects) in order to know if you have an active user session. Based on the selection result, you should determine whether to show a login modal or redirect to a content page. With initial user state set to null, you'd pop up that modal and then immediately hide it when that asynchronous call returns a session value.
With initial state set to undefined you can make that differentiation, saying:
Initially, I know nothing about my state, then it's undefined. When I know it should be empty, then I'll set it to null.
Therefor, as a practical solution, I set everything on the app's initialState to undefined. In the example above, I need to know if the login modal should be displayed after the asynchronous call resolves. skipWhile(val => val === undefined) will do the job for sure, but repeating it over and over again feels a little tedious. Plus, it's not really descriptive to our use case. I created a rxjs-custom-operators.ts with a shortened implementation:
import { Observable } from "rxjs";
import { skipWhile } from "rxjs/operators";
export const skipInitial = () => {
return <T>(source: Observable <T>): Observable<T> => {
return source.pipe(skipWhile(value => value === undefined));
};
};
Usage:
navigateOnLoad(): void {
this.store.pipe(select(selectAuthUser), skipInitial()).subscribe((authUser: CognitoUser) => {
// Navigate to login if !authUser, else navigate to content...
});
}

Resources