FLOW THE
WAY YOU LIE
GENERAL FLOW
ANNOTATION
// @flow
function concat(a: string, b: string) {
return a + b;
}
concat("A", "B"); // Works!
concat(1, 2); // Error!
function acceptsBoolean(value: boolean) {
// ...
}
acceptsBoolean(true); // Works!
acceptsBoolean(false); // Works!
acceptsBoolean("foo"); // Error!
PRIMITIVE
// @flow
function acceptsNumber(value: number) {
// ...
}
acceptsNumber(42); // Works!
acceptsNumber(3.14); // Works!
acceptsNumber(NaN); // Works!
acceptsNumber(Infinity); // Works!
acceptsNumber("foo"); // Error!
function acceptsString(value: string) {
// ...
}
acceptsString("foo"); // Works!
acceptsString(false); // Error!
VARIABLE
// @flow
let foo: number = 1;
foo = 2; // Works!
foo = "3"; // Error!
let bar = 42;
if (Math.random()) bar = true;
if (Math.random()) bar = "hello";
let isOneOf: number | boolean | string = bar; // Works!
OBJECT
// @flow
const obj1: { foo: boolean } = { foo: true };
const obj2: {
foo: number,
bar: boolean,
baz: string,
} = {
foo: 1,
bar: true,
baz: 'three',
};
const obj = {};
obj.prop = true;
obj.prop = "hello";
var val1: boolean = obj.prop; // Error!
var val2: string = obj.prop; // Works!
ARRAY
// @flow
let arr1: Array<boolean> = [true, false, true];
let arr2: Array<string> = ["A", "B", "C"];
let arr3: Array<mixed> = [1, true, "three"]
let arr: number[] = [0, 1, 2, 3];
// ?Type[] === ?Array<T> !== Array<?T>
let arr1: ?number[] = null; // Works!
let arr2: ?number[] = [1, 2]; // Works!
let arr3: ?number[] = [null]; // Error!
let arr1: (?number)[] = null; // Error!
let arr2: (?number)[] = [1, 2]; // Works!
let arr3: (?number)[] = [null]; // Works!
FUNCTION
// @flow
function concat(a: string, b: string): string {
return a + b;
}
concat("foo", "bar"); // Works!
concat(true, false); // Error!
function method(): boolean {
if (Math.random() > 0.5) {
return true;
}
} // Error!
function method(func: () => mixed) {
// ...
}
ENUM
// @flow
// 'red', 'blue', and 'green' are valid colors.
type Color = 'red' | 'blue' | 'green';
type Chip = {
// `color` can only be a valid color.
color: Color,
};
// `createChip` only takes values of type `Color`.
function createChip(color: Color): Chip {
return {
color,
};
}
FLOW GENERICS
BEHAVIOR
// @flow
function identity(value: string): string {
return value;
}
function identity<T>(value: T): T {
return value;
}
function identity(func: <T>(param: T) => T) {
// ...
}
function identity<T>(value: T): T {
value = "foo"; // Error!
return value; // Error!
}
type Item<T> = {
foo: T,
bar: T,
};
UKNOWN FLOW
ANY
// @flow
function add(one: any, two: any): number {
return one + two;
}
add(1, 2); // Works.
add("1", "2"); // Works.
add({}, []); // Works.
function getNestedProperty(obj: any) {
return obj.foo.bar.baz; // Runtime errors BUT will not be caught by Flow
}
getNestedProperty({});
function fn(obj: any) /* (:any) */ {
let foo /* (:any) */ = obj.foo;
let bar /* (:any) */ = foo * 2;
return bar;
}
let bar /* (:any) */ = fn({ foo: 2 });
let baz /* (:any) */ = "baz:" + bar;
MIXED
// @flow
function stringify(value: mixed) {
return "" + value; // Error!
}
stringify("foo");
// Figure out the type before any usage
function stringify(value: mixed) {
if (typeof value === 'string') {
return "" + value; // Works!
} else {
return "";
}
}
stringify("foo");
FLOW TYPED
SYNTAX
// @flow
declare module 'aws-sdk/dynamodb/document_client' {
import type { Request } from 'aws-sdk/request';
declare type AttributeMap = { [key: string]: AttributeValue };
declare type AttributeName = string;
declare type AttributeValue = mixed;
declare type ConditionExpression = string;
declare type ConsistentRead = boolean;
declare type ExpressionAttributeNameMap = { [key: string]: AttributeName };
declare type ExpressionAttributeValueMap = { [key: string]: AttributeValue };
declare type IndexName = string;
declare type ItemList = AttributeMap[];
declare type Key = { [key: string]: AttributeValue };
declare type KeyExpression = string;
declare type ReturnValue = 'NONE'|'ALL_OLD'|'UPDATED_OLD'|'ALL_NEW'|'UPDATED_NEW'|string;
declare type TableName = string;
declare type UpdateExpression = string;
declare interface DocumentClient {
constructor(): void;
query(params: QueryInput, cb?: (err: Error, data: QueryOutput) => void): Request<QueryOutput>;
update(params: UpdateItemInput, cb?: (err: Error, data: UpdateItemOutput) => void): Request<UpdateItemOutput>;
}
declare interface QueryInput {
TableName: TableName;
IndexName?: IndexName;
Limit?: number;
ConsistentRead?: ConsistentRead;
KeyConditionExpression?: KeyExpression;
ExpressionAttributeNames?: ExpressionAttributeNameMap;
ExpressionAttributeValues?: ExpressionAttributeValueMap;
}
declare interface QueryOutput {
Items?: ItemList;
Count?: number;
ScannedCount?: number;
LastEvaluatedKey?: Key;
}
declare interface UpdateItemInput {
TableName: TableName;
Key: Key;
ReturnValues?: ReturnValue;
UpdateExpression?: UpdateExpression;
ConditionExpression?: ConditionExpression;
ExpressionAttributeNames?: ExpressionAttributeNameMap;
ExpressionAttributeValues?: ExpressionAttributeValueMap;
}
declare interface UpdateItemOutput {
Attributes?: AttributeMap;
}
}
NAMESPASES
// @ts
declare namespace DynamoDB {
export import DocumentClient = document_client;
}
// @flow
declare module 'aws-sdk/dynamodb/document_client' {
declare interface DocumentClient { /* ... */ }
}
FLOW DOWN
JEST
// @flow
type Pojo<T> = { [string]: T };
type GenMockValue = (value?: mixed) => Function;
type GenMockFn = (fn: Function) => Function;
type GenMockVoidFn = () => void;
type MockJestFn = {
mock: Pojo<*>,
mockReturnValue: GenMockValue,
mockReturnValueOnce: GenMockValue,
mockImplementation: GenMockFn,
mockImplementationOnce: GenMockFn,
mockReset: GenMockVoidFn,
};
type MockedByJestFn<T> = T | MockJestFn;
type MockedByJestConstructor<T> = T | MockedObject;
export type MockedFn = MockedByJestFn<*>;
export type MockedObject = Pojo<MockedByJestFn<*>>;
export type MockedModule = MockedByJestConstructor<*>;
export type MockedCalls<T> = Array<Array<T>>;
JSON
// config.json
{
"messages": {
"errorPrefix": "[ERR]",
"infoPrefix": "[INFO]",
"okPrefix": "[OK]"
}
}
// @flow
const {
messages: {
okPrefix,
infoPrefix,
errorPrefix,
},
} = require('./config');
WINSTON
// @flow
export type WinstonModule = {
createLogger: (options: { transports: Array<*> }) => WinstonInstance,
transports: {
Http: Function,
Console: Function,
},
};
export type WinstonInstance = {
log: (config: WinstonLogConfig) => void,
};
export type WinstonLogConfig = {
level: string,
message: string,
applicationId: string,
correlationId: string,
};
UPDATE
MASTER FLOW
VOUCHERS
// @flow
export type ForbiddenTaskResult = {
+statusCode: 403
};
export type RequestValidationResult<T> = {
isValid: () => boolean,
getError: () => string,
getRequest: () => T,
};
export type CheckEligibilityResponse = {|
campaignName: string,
isEligible: boolean,
|};
export type SponsorshipVouchersService = {
reserveVoucher: (memberId: string, campaignName: string, randomBase: number) => Promise<ReservedVoucher>,
applyVoucher: (rawVoucherCode: string) => Promise<BusinessTaskResult<DynamoDbVoucherEntity>>,
getReservedVoucher: (memberId: string, campaignName: string) => Promise<ReserveVoucherResponse | false>
};
TEST FLOW
INTEGRATION
// @flow
import type { AxiosPromise } from 'axios';
type Pojo<T> = { [string]: T };
export type TestRequest = {
memberId?: string,
campaignName?: string,
};
export type TestResponse = {
status: string,
data: Pojo<string>,
};
export interface ITestApi {
checkEligibility: (data: TestRequest) => AxiosPromise<TestResponse>,
reserveVoucher: (data: TestRequest) => AxiosPromise<TestResponse>,
applyVoucher: (data: TestRequest) => AxiosPromise<TestResponse>,
}
FLOW TOOLS
CHECK
COVERAGE
GUIDELINES
// @flow
// Names
type foo = string;
type myFoo = string;
type JSONApiEntity = {};
// Functions
type HttpFn = () => Promise<Object>;
type DoProfilePressFn = (profile: Object) => void;
// Interfaces
interface Serializable {
serialize(): string;
}
class Foo implements Serializable {
serialize() {
return '[Foo]';
}
}
I FLOW YOU
Flow The Way You Lie
By Roman Stremedlovskyi
Flow The Way You Lie
Typed JavaScript in EPAM Systems
- 1,373