TypeScript vs Flow
Both TypeScript and Flow are very similar products and they share most of their syntax with some important differences. In this document I've tried to compile the list of differences and similarities between Flowtype and TypeScript -- specifically the syntax, usage and usability.
Disclaimer
This document might be incomplete and/or contain mistakes and was last updated to describe TypeScript 3.0 and Flow 0.80.
I'm maintaining it in my spare time, so if you find mistakes, or learn about latest additions to either project, please help keep this repo up-to-date by contributing and editing this page.
Thanks!
Differences in usage and usability
TypeScript | Flow | |
---|---|---|
Leading Design Goal / North Star | identify errors in programs through a balance between correctness and productivity | enforce type soundness / safety |
IDE integrations | top-notch | sketchy, must save file to run type-check; some IDEs have workarounds to run real-time |
type-checking speed (excluding transpilation) | benchmarks needed | benchmarks needed, in-depth description |
autocomplete |
|
|
expressiveness | great (since TS @ 2.1) | great |
type safety | very good (7 / 10) | great (8 / 10) |
specifying generic parameters during call-time (f<T>(x) ) |
yes e.g. | yes (since Flow 0.72) |
specifying generic parameters for type definitions | yes | yes |
typings for public libraries | plenty of well maintained typings | a handful of mostly incomplete typings |
unique features |
|
|
type spread operator | work in progress | shipped >=0.42 |
support for nullish coalescing proposal | no | yes |
support for decorators proposal | yes, legacy proposal | only parsing of legacy proposal, no type-checking |
support for extending built-in types | yes | no |
userland plugins | basic, not effecting emitting yet (planned) | no |
programmatic hooking | architecture prepared, work in progress | work in progress |
documentation and resources |
|
|
error quality | good | good in some, vague in other cases |
transparency | meeting notes, leadership reasoning and roadmap happens mostly publicly | low transparency, roadmap developed behind closed doors |
commercial support | no | no |
nominal and structural typing | structural | mostly structural, nominal for classes and imported opaque type aliases |
utility size (not emitted JavaScript) (latest version) |
Differences in syntax
bounded polymorphism
Flow
function fooGood<T: { x: number }>(obj: T): T {
console.log(Math.abs(obj.x));
return obj;
}
TypeScript
function fooGood<T extends { x: number }>(obj: T): T {
console.log(Math.abs(obj.x));
return obj;
}
Reference
https://flow.org/blog/2015/03/12/Bounded-Polymorphism/
maybe & nullable type
Flow
let a: ?string
// equivalent to:
let a: string | null | void
TypeScript
let a: string | null | undefined
Optional parameters implicitly add undefined
:
function f(x?: number) { }
// is semantically the same as:
function f(x: number | undefined) { }
// and also same as (the `| undefined` is redundant):
function f(x?: number | undefined) { }
Optional properties implicitly add undefined
class A {
foo?: string;
}
type casting
Flow
(1 + 1 : number);
TypeScript
(1 + 1) as number;
// OR (old version, not recommended):
<number> (1 + 1);
mapping dynamic module names
Flow
.flowconfig
[options]
module.name_mapper='^\(.*\)\.css$' -> '<PROJECT_ROOT>/CSSModule.js.flow'
CSSModule.js.flow
// @flow
// CSS modules have a `className` export which is a string
declare export var className: string;
TypeScript
declare module "*.css" {
export const className: string;
}
Reference
Exact/Partial Object Types
By default objects in Flow are not exact (can contain more properties than declared), whereas in TypeScript they are always exact (must contain only declared properties).
Flow
When using flow, { name: string }
only means “an object with at least a name property”.
type ExactUser = {| name: string, age: number |};
type User = { name: string, age: number };
type OptionalUser = $Shape<User>; // all properties become optional
TypeScript
TypeScript is more strict here, in that if you want to use a property which is not declared, you must explicitly say so by defining the indexed property. It is possible to use dotted syntax to access indexed properties since TypeScript 2.2. This is mostly a design decision as it forces you to write the typings upfront.
type ExactUser = { name: string, age: number };
type User = { name: string, age: number, [otherProperty: string]: any };
type OptionalUser = Partial<ExactUser>; // all properties become optional
Reference
Importing types
Flow
import type {UserID, User} from "./User.js";
// equivalent:
import {type UserID, type User} from "./User.js";
TypeScript
TypeScript does not treat Types in any special way when importing.
import {UserID, User} from "./User.js";
typeof
Works the same in both cases, however Flow has an additional syntax to directly import a typeof
:
Flow
import typeof {jimiguitar as GuitarT} from "./User";
// OR
import {typeof jimiguitar} from "./User.js";
type GuitarT = jimiguitar;
// OR (below also works in TypeScript)
import {jimiguitar} from "./User.js";
type GuitarT = typeof jimiguitar;
TypeScript
import {jimiguitar} from "./User";
type GuitarT = typeof jimiguitar;
Restrictive type
When you don't know a type, commonly you would use any
type. A restrictive type accepts anything, like any
but in order to use that variable you must ensure values type by refining it.
Flow
mixed
function stringifyNum(num: number) {
// Do stuff
}
function stringify(value: mixed) {
if (typeof value === 'string') {
return '' + value; // Works!
}
if (typeof value === 'number') {
return stringifyNum(value); // Works!
}
return '';
}
Reference: https://flow.org/en/docs/types/mixed/
Typescript
unknown
function stringifyNum(num: number) {
// Do stuff
}
function stringify(value: unknown) {
if (typeof value === 'string') {
return '' + value; // Works!
}
if (typeof value === 'number') {
return stringifyNum(value); // Works!
}
return '';
}
Reference: https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#new-unknown-top-type
Accessing the type of a Class
Classes are typed, so you don't need to define an explicit type for them. If you want to reference the type, you can do it the following way:
Flow
class Test {};
type TestType = typeof Test;
const instance = new Test();
type TestTypeFromInstance = Class<typeof instance>;
TypeScript
class Test {};
type TestType = typeof Test;
Keys/Props Of Type
Flow
var props = {
foo: 1,
bar: 'two',
baz: 'three',
}
type PropsType = typeof props;
type KeysOfProps = $Enum<PropsType>;
function getProp<T>(key: KeysOfProps): T {
return props[key]
}
TypeScript
var props = {
foo: 1,
bar: 'two',
baz: 'three',
}
type PropsType = typeof props
type KeysOfProps = keyof PropsType;
function getProp<T>(key: KeysOfProps): T {
return props[key]
}
Records
Flow
type $Record<T, U> = {[key: $Enum<T>]: U}
type SomeRecord = $Record<{ a: number }, string>
TypeScript
type SomeRecord = Record<{ a: number }, string>
Lookup Types
Flow
type A = {
thing: string
}
// when the property is a string constant use $PropertyType (i.e. you know it when typing)
type lookedUpThing = $PropertyType<A, 'thing'>
// when you want the property to be dynamic use $ElementType (since Flow 0.49)
function getProperty<T : Object, Key : string>(obj: T, key: Key): $ElementType<T, Key> {
return obj[key];
}
Reference:
- facebook/flow#2952 (comment)
- https://github.com/facebook/flow/commit/968210c5887b5bdd47d17167300033d1e1077d1a
- facebook/flow#2464 (comment)
- flow/try
TypeScript
Arguably, it's a bit easier to type both cases in TS, since they follow the same pattern.
type A = {
thing: string
}
type lookedUpThing = A['thing']
// and...
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // Inferred type is T[K]
}
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
Reference:
Type-narrowing functions
Flow
Note: undocumented syntax, may change:
function isNil(value: mixed): boolean %checks {
return value == null;
}
const thing = null;
if (!isNil(thing)) {
const another = thing.something;
}
Reference:
TypeScript
Type-narrowing functions are called type guard functions in TypeScript.
function isNil<T>(value: T | null): value is null {
return value == null;
}
const thing: any = null;
if (!isNil(thing)) {
const another = thing.something;
}
Getting the type of a function call return value
Flow
$Call
utility type:
type Fn1 = <T>(T) => T;
type E = $Call<Fn1, number>;
declare var e: E; // E is number
(42: E); // OK
Reference: https://github.com/facebook/flow/commit/ac7d9ac68acc555973d495f0a3f1f97758eeedb4
TypeScript
ReturnType
utility type:
type fn1<T> = (a: T) => T;
type E = ReturnType<fn1<number>>;
var e: E; // E is number
Mapped Types / Foreach Property
Flow
type InputType = { hello: string };
type MappedType = $ObjMap<InputType, ()=>number>;
Reference:
- https://gist.github.com/gabro/bb83ed574690645053b815da2082b937
- https://twitter.com/andreypopp/status/782192355206135808
TypeScript
A bit more flexibility here, as you have access to each individual key name and can combine with Lookup types and even do simple transformations.
type InputType = { hello: string };
type MappedType = {
[P in keyof InputType]: number;
};
Function and method overloading
Flow
It is possible to declare multiple signatures for the same method (also called: overloading). This feature is undocumented, and only available in type declarations (.js.flow
files or module statements), not inline/alongside your code.
declare function add(x: string, y: string): string;
declare function add(x: number, y: number): number;
declare class Adder {
add(x: string, y: string): string;
add(x: number, y: number): number;
}
However, it's possible to create function overloads inline for functions outside of classes, by using additional declarations.
declare function add(x: string, y: string): string;
declare function add(x: number, y: number): number;
function add(x, y) {
return x + y;
}
add(1, 1); // Ok
add("1", "1"); // Ok
add(1, "1"); // Error
TypeScript
TypeScript supports both function and method overloading, in both: type definitions (.d.ts
) and inline alongside code.
class Adder {
add(x: string, y: string): string;
add(x: number, y: number): number;
add(x, y) {
return x + y;
}
}
function add(x: string, y: string): string;
function add(x: number, y: number): number;
function add(x, y) {
return x + y;
}
Read-only Types
Flow
type A = {
+b: string
}
let a: A = { b: 'something' }
a.b = 'something-else'; // ERROR
TypeScript
type A = {
readonly b: string
}
let a: A = { b: 'something' }
a.b = 'something-else'; // ERROR
One caveat that makes TypeScript's readonly
less safe is that the same non-readonly
property in a type is compatible with a readonly
property. This essentially means that you can pass an object with readonly
properties to a function which expects non-readonly properties and TypeScript will not throw errors: example.
"Impossible flow" type
Flow
empty
function returnsImpossible() {
throw new Error();
}
// type of returnsImpossible() is 'empty'
TypeScript
never
function returnsImpossible() {
throw new Error();
}
// type of returnsImpossible() is 'never'
Difference types
Flow
type C = $Diff<{ a: string, b: number }, { a: string }>
// C is { b: number}
Note however that $Diff is not an official feature.
It only works properly as lower bound, i.e. you can assign something to it, but can't use it after that.
(source)
Typescript
You can define your own filter type, but it does not have a helper type for that.
class A {
a: string;
b: number;
}
class B {
a: string;
c: boolean;
}
type Omit<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
//
type C = Omit<A, B>;
// C is { b: number }
However, Flow implementation is stricter in this case, as B have a property that A does not have, it would rise an error. In Typescript, however, they would be ignored.
Same syntax
Most of the syntax of Flow and TypeScript is the same. TypeScript is more expressive for certain use-cases (advanced mapped types with keysof, readonly properties), and Flow is more expressive for others (e.g. $Diff
).
optional parameters
Flow and TypeScript
The syntax in either tool is the same - question mark: ?
suffixing the parameter name:
function(a?: string) {}
call-time generic parameters
In TypeScript and Flow (since version 0.72) you may use specify the type of a generic when calling the generic function or the constructor.
const set = new Set<string>();
Or using a more complex behavior:
function makeTgenerator<T>() {
return function(next: () => T) {
const something = next();
return something;
}
}
const usage = makeTgenerator<string>()
// 'usage' is of type: (next: () => string) => string
TypeScript-only concepts
this
in functions (outside of objects)
Declarable arbitrary function something(this: { hello: string }, firstArg: string) {
return this.hello + firstArg;
}
Private and Public properties in classes
class SomeClass {
constructor(public prop: string, private prop2: string) {
// transpiles to:
// this.prop = prop;
// this.prop2 = prop2;
}
private prop3: string;
}
Non-null assertion operator
Add !
to signify we know an object is non-null.
// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
// Throw exception if e is null or invalid entity
}
function processEntity(e?: Entity) {
validateEntity(e);
let s = e!.name; // Assert that e is non-null and access name
}
Conditional Typing
type XorY<T, U> = T extends U ? X : Y;
This alone, introduces new helper types, or types aliases.
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : any;
Mapped Type Modifiers
You can use +
and -
operators to modify mapped types.
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
interface Foo {
readonly abc: number;
}
// 'abc' is no longer read-only.
type TotallyMutableFoo = Mutable<Foo>
Helper type modifiers
Required
is a type mapper to make all properties of an object to be required.
Partial
is a type mapper to make all properties of an object to be optional.
Readonly
is a type mapper to make all properties of an object to be readonly.
Flow-only concepts
Inferred existential types
*
as a type or a generic parameter signifies to the type-checker to infer the type if possible
Array<*>
Variance
https://flow.org/en/docs/lang/variance/
function getLength(o: {+p: ?string}): number {
return o.p ? o.p.length : 0;
}
Bivariance is among the design decisions driving TypeScript.
Useful References
- microsoft/TypeScript#1265
- Undocumented Flow modifiers facebook/flow#2464
- http://sitr.us/2015/05/31/advanced-features-in-flow.html