Skip to content

Instantly share code, notes, and snippets.

@voltrevo
Last active November 8, 2016 00:47
Show Gist options
  • Save voltrevo/ecb5b7292707d29b13ae453ae0b529d9 to your computer and use it in GitHub Desktop.
Save voltrevo/ecb5b7292707d29b13ae453ae0b529d9 to your computer and use it in GitHub Desktop.

Comparison of Typescript and Flow

Common Features

  • Lots of type inference

  • Null/undefined checking

Similar syntax As an example, I ported my [EventEmitter](https://github.com/voltrevo/voltrevo-event-emitter) module to [Typescript](https://github.com/voltrevo/voltrevo-event-emitter-ts) and [Flow](https://github.com/voltrevo/voltrevo-event-emitter-flow) and they are almost identical.

Advantages of Typescript

  • Does a lot for you, especially when paired with VS Code, so your project requires less set-up

  • Type annotations available for many more third party libraries than Flow

Type information on hover is complete ```js class Actor { act(description: string) { return { undo() { console.log(`undo ${description}`); }, }; } }

const actor = new Actor();

// Typescript says that action is a { undo: () => void }, flow just says {} const action = actor.act('foo');

// Strangely though, flow does know that undo is a () => void const undo = action.undo;


In general, VS Code is also able to provide a much better intellisense experience than any Flow plugin available for Atom/Sublime/VS Code. Flow seems similarly capable under the hood though, so it should get better.
</details>

<details>
<summary>Type inference works with generics</summary>
```js
function identity<T>(value: T) {
  return value;
}

// Typescript knows that foo is a string. Flow does not. Flow also emits an error if you try to
// add a string type annotation to foo.
const foo = identity('foo');
Better support for 100% typed code via `--noImplicitAny` ```js // While Flow sometimes asks for type information, e.g. for parameters of exported functions, // there are many cases where the `any` type can sneak into your code and prevent type checking. // A common one is missing an index signature of an object:

const map = { foo: 'bar' };

function get(key: string) { // Typescript: Element implicitly has an 'any' type because type '{ foo: string; }' has no index // signature. // Flow: Exports get(key: string): any without warning. return map[key]; }


However, much of this gap can be closed by using [flow-coverage-report](https://www.npmjs.com/package/flow-coverage-report):

$ flow-coverage-report -i 'src/.js' ┌───────────────────────┬─────────┬───────┬─────────┬───────────┐ │ filename │ percent │ total │ covered │ uncovered │ │ src/Collection.js │ 100 % │ 38 │ 38 │ 0 │ │ src/MapWithDefault.js │ 100 % │ 28 │ 28 │ 0 │ │ src/index.js │ 100 % │ 59 │ 59 │ 0 │ │ src/mapTests.js │ 87 % │ 8 │ 7 │ 1 │ │ src/useMap.js │ 75 % │ 4 │ 3 │ 1 │ └───────────────────────┴─────────┴───────┴─────────┴───────────┘ ┌─────────────────────────┬──────────────────────────────────────────┐ │ included glob patterns: │ src/.js │ │ excluded glob patterns: │ node_modules/** │ │ threshold: │ 80 │ │ generated at: │ Mon Nov 07 2016 16:02:09 GMT+1100 (AEDT) │ │ flow version: │ 0.34.0 │ │ flow check passed: │ yes (0 errors) │ └─────────────────────────┴──────────────────────────────────────────┘ ┌─────────────────────────────┬─────────┬───────┬─────────┬───────────┐ │ project │ percent │ total │ covered │ uncovered │ │ voltrevo-event-emitter-flow │ 98 % │ 137 │ 135 │ 2 │ └─────────────────────────────┴─────────┴───────┴─────────┴───────────┘


Some may actually prefer this to ease the transition into adding typing to a project. On the other hand, others may prefer temporarily turning on Typescript's `--noImplicitAny` to view errors in the editor which would give them feedback in real-time as they add type information.
</details>

<details>
<summary>Correctly identifies `.pop` on a `T[]` as a `T | undefined`</summary>
```js
const nums = [17];
nums.pop();

const num = nums.pop();

// Typescript correctly emits an error here because it has inferred `num` as a `number | undefined`.
// Flow doesn't catch it because it infers `num` as `number`.
console.log(num.toString());
Supports interfaces ```js interface X {} interface Y {}

// Typescript: OK. Flow: Error: implements not supported. class Widget implements X, Y {}


Flow can parse the `interface X { ... }` syntax, but without `implements` it seems equivalent to just defining a type (and you'll need [babel-plugin-transform-flow-strip-types](https://www.npmjs.com/package/babel-plugin-transform-flow-strip-types)).
</details>

## Advantages of Flow

- Provides type checking only, so it is more flexible about how you set-up the rest of your project

<details>
<summary>Catches the error when you assign a `Derived[]` to a `Base[]`</summary>
```js
class Animal {}

class Cat extends Animal {
  furColor: string;
}

const cats: Cat[] = [];
const animals: Animal[] = cats;

// Flow correctly emits an error here. Typescript does not. It's an error because it makes cats
// no longer conform to type Cat[].
animals.push(new Animal());

// As an example, badFurColor below is inferred as a string, and furColor is
// provided via autocomplete, even though cats[0] is not a cat.
const badFurColor = cats[0].furColor;
Infers types based on usage (not just known input -> operation -> inferred output) ```js function double(x) { return x * 2; // Flow infers x as string because of usage below, and emits an error here }

const result = double("foo");

**Note**: With `noImplicitAny`, Typescript emits an error for `x` being implicitly the `any` type.
</details>

<details>
<summary>Doesn't confuse `{}` with `string` and `number`</summary>
```js
// Flow (arguably) correctly emits an error saying string is incompatible with object.
// Typescript doesn't mind.
const foo: {} = 'foo';

// There is a Typescript bug related to this that causes it to emit an error for this code,
// Flow handles it fine though:
const items = ['foo', {}, null];

for (const el of items) {
  if (typeof el === 'string') {
    console.log(el.toUpperCase());
  }
}

More information: microsoft/TypeScript#12077

Missing Features in Both Typescript and Flow

Mistypes elements of `T[]` accessed via subscript as `T`. They should be `T | undefined` due to the out of bounds case ```js const nums = [1, 2, 3]; const foo: number = nums[100];

console.log(foo.toString()); // boom not prevented

</details>

<details>
<summary>Can't declare types in class scope</summary>
```js
class Foo<T> {
  type Wrapper = { value: T }; // Both error: 'type' not expected
}
Doesn't catch uninitialized member variables ```js class DefaultGetter { generateDefault: () => string;

get() { // Both typescript and flow don't catch that generateDefault is undefined return this.generateDefault(); } }

const defaultGetter = new DefaultGetter(); const foo = defaultGetter.get();

</details>

<details>
<summary>Allows index signatures with non-nullable value types</summary>
```js
const map: { [key: string]: string } = {
  foo: 'bar',
};

const key = 'idontexist';

// Both Typescript and Flow fail to catch this
const oops = map[key].toUpperCase();

This is similar to the array subscript issue. In both cases it shouldn't be possible to have the subscript operator return a non-nullable type, because arrays and objects are unable to define a value for all possible subscripts.

Other Notable Differences

  • In Typescript, the type of undefined is undefined, in Flow the type of undefined is void
@halbgut
Copy link

halbgut commented Nov 7, 2016

Can't say much about TypeScript, since I don't know it that well but as for Flow, there are some errors in this comparison.

Correctly identifies .pop on a T[] as a T | undefined

The TypeScript playground says otherwise. I'm probably wrong, can you elaborate?

Allows T | undefined types

The same is true for Flow. You just need to use void or alternatively ?[something]. Which is nicer than undefined IMO, because undefined is a special case in JS. Example

Mistypes elements of T[] accessed via subscript as T. They should be T | undefined due to the out of bounds case
This is not so much a missing feature as it is a conciencous design descicion AFAIK.

Allows index signatures with non-nullable value types

I'd just like to point out the difference flow makes between objects and maps. What you defined is a map for flow, meaning it's treated similarly to an array. Objects on the other hand are type-checked just fine in flow, while they aren't in TypeScript (again, not an expert). Flow, TypeScript

Disclaimer: I'm not an expert in any of these two languages.

@DanielRosenwasser
Copy link

@Kriegslustig - the playground doesn't currently enable strictNullChecks mode.

@voltrevo
Copy link
Author

voltrevo commented Nov 8, 2016

@Kriegslustig

  • As @DanielRosenwasser pointed out about the typescript playground
  • You're right about Flow working with T | undefined, I didn't realize Flow uses void instead of undefined for the typename, so T | void is the equivalent. I removed the point and added a new one under 'Other Notable Differences', thanks!
  • On the distinction between objects and maps, I think both Typescript and Flow have this distinction. I really like how Flow checks the exact key 'idontexist' and gives an error in the example you provide, however realistically the exact key is unknown, and here Flow falls down again:
const map = {
  foo: 'bar',
};

function getUpper(key: string) {
  // Typescript: map[key] is implicitly any, need an index signature. Flow: Ok.
  return map[key].toUpperCase();
}

So --noImplicitAny catches both cases, but Flow's approach only gets the one with the statically determined key. However, if you're not using the subscript operator then an index signature shouldn't be used, and both Typescript and Flow will treat it as an object and ensure you don't accidentally use a property that's not there:

const obj = {
  foo: 'bar',
};

// Both Typescript and Flow complain that idontexist is not a property of obj
console.log(obj.idontexist);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment