Sometimes we just want to declare the types of some definitions, so that code that uses those definitions may be typechecked, without worrying about the code that implements those definitions. The implementation code may simply not be available for typechecking (such as for JavaScript built-ins like
Math or
Array). Alternatively, the implementation code may be available but we may not want to typecheck it (yet). Declarations also enable a powerful modular approach to typechecking libraries, separating the checking of uses from the checking of implementations.
As we shall see below, declarations can not only express types of definitions in global scope (variables, functions, classes), but also express signatures of modules.
Note that declarations are exclusively compile-time entities: they are transpiled away, so they have no effect at run time. In fact, declarations without backing implementations may cause typechecked uses to break at run time! Thus, it is important to distinguish declaration code from regular code.
In Flow, there are two recommended styles of doing declarations: let’s call them the “.flowconfig”-style and the “.js.flow” style. The styles differ in their generality and convenience, but share some important characteristics: both involve locating the declarations separately from regular code, and both use similar syntax.
Let’s start with the “.flowconfig” style of doing declarations.
In this style, declarations (see below for examples) can be made visible to an entire project by putting them in files with extension
.js in a directory, say
decls, and putting the following in the
.flowconfig of the project:
[libs] decls/
Here’s a simple example, where we declare a global variable.
declare var DEBUG: bool;
$> flow
1: declare var DEBUG: bool; ^^^ Unexpected token var
Other global definitions, like functions and classes, can similarly be declared. Note how function and method bodies are elided.
declare function isLeapYear(year: string): bool; declare class Counter { val: number; incr(): void; }
Such declarations can be made visible to all modules in a project (see above). This means that any code that uses these definitions in a typesafe manner will typecheck.
if (isLeapYear('2016')) console.log('Yay!');
Note that it is entirely up to the programmer to ensure that declared definitions actually exist, and have the correct types.
You can declare a class which mixes in 1 or more other classes with the
mixins keyword. Mixing class
B into class
A copies
B’s fields and methods into
A. Note, however, that any fields or methods that
B inherits are not copied over. Mixins are for code reuse, not for multiple inheritance.
// You can mixin more than one class
It is similarly useful to declare types. Like other declarations, type declarations can also be made visible to all modules in a project.
Declared types include type aliases and interfaces; in fact, they share the same syntax as usual types, so the
declare keyword is superfluous.
declare type Response = 'yes' | 'no' | 'maybe'; declare interface Stack<T> { push(item: T): void; pop(): T; isEmpty(): bool; }
Finally, modules can be declared as well. Like definitions that should exist at run time, it is up to the programmer to ensure that the declared modules actually exist at run time.
Declaring a module consists of naming the module and declaring its exports. Of course, the exports can be variables, functions, classes, as well as type aliases and interfaces. Thus it is not surprising that they share exactly the same syntax as the declarations above, except that they are now scoped to the module, instead of globally.
declare module Misc { declare var DEBUG: bool; declare function isLeapYear(year: string): bool; declare class Counter { val: number; incr(): void; } declare type Response = 'yes' | 'no' | 'maybe'; declare interface Stack<T> { push(item: T): void; pop(): T; isEmpty(): bool; } }
Note that a declared module should have a global name. (This can sometimes be a limitation; see below.) The global name could be any string, though: it need not be an identifier. So the following also works:
declare module "fancy-pants" { // ... }
A declared module would be looked up by the typechecker only when a file implementing that module was not found by the resolution algorithm of the module system, or such a file was found but not checked. For example, suppose we have the following code in a file
src/LookBeforeYouLeap.js:
// @flow import { isLeapYear } from 'Misc'; if (isLeapYear('2016')) console.log('Yay!');
If the import successfully resolves to (say) a file
src/node_modules/Misc/index.js with the following code, then we would get a Flow error, since the parameter of
isLeapYear expects a
number.
// @flow export function isLeapYear(year: number): bool { return (year % 4 == 0); // yeah, this is approximate }
But on the other hand, if the above file doesn’t exist or we remove the
@flow header then we would not get a Flow error, since the parameter of
isLeapYear in the declared module
Misc expects a
string.
Now, let’s look at a more general, and sometimes more convenient, way to declare types for modules: the “.js.flow” style.
The exported types of a module may be declared in a declaration file with the
.js.flow extension, collocated with the corresponding implementation file with the
.js extension. A declaration file for a module shadows a collocated implementation file for that module when typechecking other code that may depend on that module.
For example, looking back at the file
src/LookBeforeYouLeap.js, suppose it instead had the following code (note the relative-path import):
// @flow import { isLeapYear } from './Misc'; if (isLeapYear('2016')) console.log('Yay!');
Next, suppose that
src/Misc.js had an incompatible implementation of
isLeapYear, just as above.
// @flow export function isLeapYear(year: number): bool { return (year % 4 == 0); // yeah, this is approximate }
If we now create a declaration file
src/Misc.js.flow, the declarations in it will be used instead of the code in
src/Misc.js. Let’s say we have the following declarations in
src/Misc.js.flow. Note that the syntax for declarations in a declaration file is the same as we’ve seen above, except that we also write the keyword
export, like we would in an implementation file.
// @flow declare export function isLeapYear(year: string): bool;
What do you think will happen? Right, the
isLeapYear call in
src/LookBeforeYouLeap.js will typecheck.
As this example shows, declaration files must be written with care: it is up to the programmer to ensure they are correct, otherwise they may hide type errors.
That said, declaration files provide a very convenient way to write specifications for modular typechecking. Sometimes, the implementation code may not yet be free of type errors, but we may want to move on and come back to fixing the type errors later. Another important use of this feature is for libraries, whose implementation code may be too complex to typecheck satisfactorily, but whose clients we still want to typecheck against well-defined specifications.
As noted above, declarations should be distinct from regular code. But sometimes, it is useful to do declarations “inline,” as part of the source of an implementation file. Proceed with caution!
The most common use is writing “work-in-progress” code while ensuring that your code typechecks. In the following example, say you want to finish writing the function
fooList without bothering to mock up its dependencies first: a function
foo that takes a
number, and returns a
string and a class
List that has a
map method. Easy! (Just don’t forget to replace the declarations with proper implementations.)
declare class List<T> { map<U>(f: (x: T) => U): List<U>; } declare function foo(n: number): string; function fooList(ns: List<number>): List<string> { return ns.map(foo); }
© 2013–present Facebook Inc.
Licensed under the BSD License.