Skip to content

Instantly share code, notes, and snippets.

@busypeoples
Last active July 17, 2023 10:12
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save busypeoples/61e83a1becc9ee9d498e0db324fc641b to your computer and use it in GitHub Desktop.
Save busypeoples/61e83a1becc9ee9d498e0db324fc641b to your computer and use it in GitHub Desktop.
Flow Fundamentals For JavaScript Developers
//
// MIT License
//
// Copyright (c) 2018 Ali Sharif
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// @flow
// Flow Fundamentals For JavaScript Developers
/*
Tutorial for JavaScript Developers wanting to get started with FlowType.
Thorough walkthrough of all the basic features.
We will go through the basic features to gain a better understanding of the fundamentals.
You can uncomment the features one by one and work through this tutorial.
If you have any questions or feedback please connect via Twitter:
A. Sharif : https://twitter.com/sharifsbeat
*/
// Current Version: Flow v0.73
// SETUP
/*
* Always refer to the official documentation for a deeper understanding.
https://flow.org/en/
* Installation and Setup:
https://flow.org/en/docs/install/
* Online Try Repl:
https://flow.org/try/
* IDE Plugins:
Setup Flow for your IDE
https://flow.org/en/docs/editors/
*/
// Basics
/*
Let's begin with a couple of very basic examples.
*/
const a = 'foo'
const b = 1
const c = false
const d = [1, 2, 3]
const e = ['a', 'b', 'c']
const f = { id: 1 }
const g = null
const h = undefined
/*
Flow offers a number of types.
To get a quick overview, let's be explicit and define types for the above constants.
`const a : string = 'foo'` (ok)
---------------------------------------------------------------------
`const a : number = 'foo'` (error)
We will get the following error:
const a : number = 'foo'
^^^^^ string. This type is incompatible with
---------------------------------------------------------------------
So assign the type for learning purpoeses. In reality you would rely on type inference.
*/
// const aTyped: string = 'foo'
// const bTyped: number = 1
// const cTyped: boolean = false
/*
The first three are relatively clear. But how do we type `d`?
*/
// const dTyped: number[] = [1, 2, 3]
// or
// const dTyped : Array<number> = [1, 2, 3]
/*
Let's continue:
*/
// const eTyped: Array<string> = ['a', 'b', 'c']
// const fTyped: Object = { id: 1 }
// const gTyped: null = null
/*
What about undefined?
In FlowType you can use `void` type to declare a value as undefined.
const h : void = undefined
We will get into more detail as we progress and cover primitives in more detail.
*/
/*
Let's continue and see what else FlowType has to offer.
For example the above `i` and `j` could either be assigned a primitve type, but interestingly
a literal type as well. How would that look like?
*/
// const iTyped: 2 = 2
// const jTyped: 'foo' = 'foo'
/*
Now you might be wondering what value we gain from literal types?
We can constraint what values we expect.
*/
// type ExpectedInput = 1 | 2 | 3
//
// const doSomething = (input: ExpectedInput) => {
// switch (input) {
// case 1:
// return 'Level 1'
// case 2:
// return 'Level 2'
// case 3:
// return 'Level 3'
// }
// }
//
// doSomething(0) // error: This type is incompatible with the expected param type of number enum
// doSomething(1) // ok
/*
We were dealing with const variables up until now. What about let or var. While
const variables can't be reassigned, which means FloeType can interfere the type and know for sure it will never change.
This is different with let or var.
*/
// let aVar : string = 'foo'
//
// aVar = 'bar'
//aVar = 1 // Error!
/*
As we can see in the above example, once you assign a type to a let or var variable any re-assignment
has to be of that same type otherwise Flow will complain.
*/
/*
Take a look at the following example:
---------------------------------------------------------------------
const i : 3 = 2
^ number. Expected number literal `3`, got `2` instead
---------------------------------------------------------------------
*/
// Any Vs. Mixed
/*
Sometimes you can't tell what the exact type is or you are currently converting an existing
non-typed code base. Here is where `any` and `mixed` are helpful. It's important to note
that they furfill different puroposes. `any` should be used as a last resort, as it skips type checking.
In contrast `mixed` is useful when you can't be sure what the input type is, i.e:
*/
// const double = (input: mixed) => {
// if (typeof input === 'string') {
// return input + ' - ' + input
// }
// if (Array.isArray(input)) {
// return input.concat(input)
// }
// return input
// }
//
// const result = double('foo') // ok
/*
We need to refine the input by checking the type and then returning an appropriate value else Flow will complain.
With `any` we completely bypass the type checker. We can pass in any value to `length` and will never receive an error.
As already mentioned use `any` as a last resort if possible.
*/
// const length = (input: any) => {
// if (typeof input === 'string') {
// return input.length
// }
//
// if (Array.isArray(input)) {
// return input.length
// }
//
// return false
// }
// const bar = length('foo')
// const baz = length([1, 2, 3, 4])
// const foo = length(1) // no Error!
// Optional Values
/*
Sometimes we want a certain value to be optional.
For example take a look at the following function:
*/
// const optionalLength = (input: ? string | Array<any>) => {
// if (typeof input === 'string') {
// return input.length
// }
//
// if (Array.isArray(input)) {
// return input.length
// }
//
// return false
// }
//
// optionalLength()
// optionalLength(null)
// optionalLength(undefined)
// optionalLength([1, 2, 3, 4])
// optionalLength('foo')
/*
As we can see, we can call optionalLength with undefined, null, an array or a string.
But as you would expect, passing in a number would cause an error.
*/
// optionalLength(1) // Error!
// Functions
/*
Now that we have covered the very basics, it's time to get more advanced.
We have already seen a couple of functions in the previous section, but let's take a more
detailed look at Function types.
First off all, we would like to type the input and output of a function, so let's see
how this is done.
*/
// let add = (a: number, b: number) : number => {
// return a + a
// }
//
// add(2, 2)
// add(2, 'foo') // Error!
// const addResult : number = add(2, 2)
/* Try running the follwing: */
// const addResultError : string = add(1, 2)
/*
You will see the following error:
---------------------------------------------------------------------
Cannot assign add(...) to addResultError because number is incompatible with string
---------------------------------------------------------------------
*/
/*
In case you want to use tradional function declarations as opposed to arrow functions,
you can write the previous example like so:
*/
// function addFunction(a: number, b: number) : number {
// return a + a
// }
//
// addFunction(2, 2)
// addFunction(2, 'foo') // Error!
// const addFunctionResult : number = addFunction(2, 2)
/*
For more information check:
https://flow.org/en/docs/types/functions/
*/
// Arrays
/*
Let's continue with arrays.
If you recall at the very beginning, we typed a simple array. There are two ways to type an array:
Array<Type> or Type[].
So f.e. these two are equivalent:
*/
// const aArray : Array<number> = [1, 2, 3]
// const aArrayShortHand : number[] = [1, 2, 3]
/*
What if we might have a null value inside our array. The answer is very similar to
what we have seen in the Optional section.
*/
// const aOptionalArray : Array<?number> = [1, null, 2, undefined]
// const aOptionalArrayShortHand : (?number)[] = [1, null, 2, undefined]
/*
What if we want to be more specific with our array definition?
Take the follwing example:
We have an array consisting of exactly three items, in short a tuple containg a string, a number and another string:
['foo', 1, 'bar']
*/
// const tupleA : [string, number, string]= ['foo', 1, 'bar']
// const tupleB : [string, number, number]= ['foo', 1, 'bar'] // Error!
/*
Another important aspect is that once you have a tuple defined, you can't use any
of the existing Array methods which mutate the array. Flow will complain at the next example:
*/
// tupleA.push('foobar')
/*
---------------------------------------------------------------------
Cannot call tupleA.push because property push is missing in $ReadOnlyArray
---------------------------------------------------------------------
So once you define a tuple it becomes read-only as opposed to an array, which can be
observed quite well via the next code snippet:
*/
// const bArray : Array<number> = [1, 2, 3]
// bArray.push(4)
// bArray.push('foo') // Error!
/*
For more information check:
https://flow.org/en/docs/types/arrays/
https://flow.org/en/docs/types/tuples/
*/
// Objects
/*
Let's take a look at how Flow works with objects.
*/
// const aObject : Object = {id: 1, name: 'foo'}
// const bObject : {id: number} = {id: 1, name: 'foo'}
// const cObject : {id: number, name: string} = {id: 1, name: 'foo'}
// const dObject : {id: number, name: string, points: number} = {id: 1, name: 'foo'} // Error!
/*
`dObject` will casue an error as points is not defined. We want to make points optional.
We've already seen how to make a value optional, so let's see how to achieve the same for
an object property.
*/
// const dRefinedObject : {id: number, name: string, points?: number} = {id: 1, name: 'foo'}
/*
By declaring `points?: number`, we are saying that points might not be defined.
To make things more readible, you will probably resort back to defining a type alias
for the object declaration. This is especially helpful if you also plan to reuse a type definition.
*/
// type E = {id: number, name: string, points?: number}
// const eObject : E = {id: 1, name: 'foo'}
/*
Another important thing to note when working with objects and Flow, is that there is
a concept of sealed vs. unsealed objects. Take a look at the following code snippets:
*/
// const fObject = {
// id: 1
// }
//
// fObject.name = 'foo' // Error!
/*
So the above doesn't work. Per definition a sealed object is an object with defined properties.
An unsealed object is defined as `{}`, an object containing no properties.
Now we can add a property the object, without FlowType complaining.
*/
// const gObject = {}
// gObject.name = 'foo'
/*
Another important aspect when working with objects is that we don't have to be exact with
our type definition. See the next example:
*/
// type F = {id: number, name: string}
// const fObject : F = {id: 1, name: 'foo', points: 100} // No Error!
/*
But what if wanted to be exact?
We can be exact by wrapping our definition inside `{|...|}` like so:
*/
// type G = {|id: number, name: string|}
// const gObject : G = {id: 1, name: 'foo', points: 100} // Error!
// const gOtherObject : G = {id: 1, name: 'foo'} // No Error!
/*
Another possibility is to use the `$Exact<T>` utility helper provided by Flow.
*/
// type H = $Exact<{id: number, name: string}>
// const hObject : H = {id: 1, name: 'foo', points: 100} // Error!
/*
A common approach in JavaScript is to use objects as a map, again FlowType offers
a convenient way to type a map.
*/
// const aMap : {[number] : string} = {}
// aMap[1] = 'foo'
// aMap['a'] = 'foo' // Error!
// aMap[1] = 1 // Error!
// const otherMap : {[string]: number} = {}
// otherMap['foo'] = 1
// otherMap[1] = 2 // Error!
// otherMap['bar'] = 'foo' // Error!
/*
We can also mix property declarations with dynamic key/value pairs:
*/
// const mixedMap : {
// id: number,
// [string]: number
// } = {
// id: 1
// }
//
// mixedMap['foo'] = 1
// mixedMap[1] = 2 // Error!
// mixedMap['bar'] = 'foo' // Error!
/*
For more information check:
https://flow.org/en/docs/types/objects/
*/
// Classes
/*
There is not much needed to know to be able to type classes with Flow.
You can refer to the class itself when typing a class instance.
*/
//
// class Foo {
// state = {val: 0}
// update(val) {
// this.state = {val}
// }
// getVal() {
// return this.state.val
// }
// }
//
// const foobar : Foo = new Foo()
/*
Class methods and properties can be typed like you would expect.
Let's update the previous example.
*/
// class Foo {
// state : {val: number} = {val: 0}
// update(val:number) : void {
// this.state = {val}
// }
// getVal() : number {
// return this.state.val
// }
// }
//
// const foobar : Foo = new Foo()
//
// foobar.update(3)
// foobar.update('foo') // Error!
//
// const fooResult : number = foobar.getVal()
// const fooResultError : string = foobar.getVal() // Error!
/*
To round things off, let's also take a look at interfaces.
What if we had a class Bar, that also had a state property and an update function?
*/
// interface Updateable<T> {
// state: {val: T};
// update(a: T) : void
// }
//
// class InterfaceExample implements Updateable<boolean> {
// state = {val: false}
// constructor(val) {
// this.state = {val}
// }
// update(val) {
// this.state = {val}
// }
// getValue() {
// return this.state.val
// }
// }
//
// const exampleInstance = new InterfaceExample(true)
// const exampleInstanceResultOk : boolean = exampleInstance.getValue()
// const exampleInstanceResultError : number = exampleInstance.getValue() // Error!
/*
For more information check:
https://flow.org/en/docs/types/classes/
https://flow.org/en/docs/types/interfaces/
*/
// Generics
/*
Now we're getting into more advanced territory here. Up until now we should
have covered all the necessary basics.
Let's continue with our previous example and add generics. For example our Example class might als
accept a string instead of a number. We want to abstract the type definition in this case.
*/
// class Example<A> {
// state : {val: A}
// constructor(input: A) {
// this.state = {val: input}
// }
// update(val:A) : void {
// this.state = {val}
// }
// getVal() : A {
// return this.state.val
// }
// }
//
// const exampleGenericNumber : Example<number> = new Example(1)
// exampleGenericNumber.update(2)
// const exampleGenericResult : number = exampleGenericNumber.getVal()
//
// const exampleGenericString = new Example('one')
// const exampleGenericResultOther : string = exampleGenericString.getVal()
/*
If you uncommented the above example you will notice that everything works.
Interestingly you don't even have to explicity define a type for `const exampleGenericString = new Example('foo')`
Flow will know that our return value is a string, as can be seen in the following line.
We can do a lot more with generics, like f.e. define type aliases or functions.
Let's see some examples to get a better idea of the possibilities.
*/
// type ExampleGeneric<A, B> = {
// one: A,
// two: B
// }
//
// const GenericAlias : ExampleGeneric<number, boolean> = {
// one: 1,
// two: false
// }
//
// const GenericAliasError : ExampleGeneric<number, boolean> = {
// one: 1,
// two: 'foo'
// } //Error!
/*
Generics with Functions
*/
// const doubleIfPossible = <A>(a: A) : A => {
// if (typeof a === 'string' || typeof a === 'number') {
// return a + a
// }
// return a
// }
//
// const doubleIfPossibleResultOne : number = doubleIfPossible(2)
// const doubleIfPossibleResultTwo : string = doubleIfPossible('foo')
// const doubleIfPossibleResultError : string = doubleIfPossible(true) // Error!
/*
There is a lot more you can do with generics. If you're interested to find out more, then consult
the official Flow documentation.
*/
/*
For more information check:
https://flow.org/en/docs/types/generics/
*/
/* Read-only */
/*
Sometimes we want to make sure that object properties are read-only.
Take a look at the following example:
*/
// type AB = { a: number, b: string };
//
// const readOnlyNone = (o: AB) => {
// o.a = 100 // No Error!
// return o
// }
/* We can rewrite our `a` propety to `+a` and ensure that `a` is read-only. */
// type ReadOnlyA = { +a: number, b: string };
//
// const readOnlyA = (o: ReadOnlyA) => {
// o.a = 100; // Error!
// o.b = 'test' // No Error!
// return o
// }
/*
Now if we want to make our object readonly, we need to make sure that all
properties are read-only.
*/
// type ReadOnlyAB = { +a: number, +b: string };
//
// const readOnlyAB = (o: ReadOnlyAB) => {
// o.a = 100; // Error!
// o.b = 'test' // Error!
// return o
// }
/*
Sine flow v.0.59 we can use `$ReadOnly<T>` to make an object read-only.
The above example can be rewritten to:
*/
// type ReadOnlyAB = $ReadOnly<{ a: number, b: string }>;
//
// const readOnlyAB = (o: ReadOnlyAB) => {
// o.a = 100; // Error!
// o.b = 'test' // Error!
// return o
// }
// Intersection
/*
Sometimes we need to define a type that a set of other types.
Take the a look at the following example.
*/
// type InterA = {id: number}
// type InterB = {name: string}
// type InterC = {email: string}
//
// type User = InterA & InterB & InterC
// const addUser = (users: Array<User>, user: User) : Array<User> => [...users, user]
// addUser([], {id: 1, name: 'user a'}) // Error!
// addUser([], {id: 1, name: 'user b', email: 'some.email@some.email'})
/*
This is it for now, you should have a good understanding of FlowType now.
This tutorial will be updated from time to time.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment