export type LearningTimeInfo = $ReadOnly<{|
// The number of seconds offset from the learner UTC time to their
// local time, to allow for in/out school detection.
localTimeOffsetSeconds: {offsetSeconds: number},
// The full URL. Intended for debugging/human-readability. Code
// should not be analyzing this field to do things! -- instead we
// should be adding a new field to the protobuf that holds the
// needed data.
urlForDebugging: string,
// What product is the learner using? Test-prep, missions, etc.
product?: LearningTimeProduct,
// Activity to which Learning Time will be credited.
activity?: LearningTimeActivity,
// Attributes of the learner's school(s); null if this learner is not
// assigned to a school. A learner may have multiple schools; index 0
// will always be the "primary" school with which they are associated.
schoolInfo?: $ReadOnlyArray<LearningTimeSchoolInfo>,
// Associated content path, if one exists.
content?: LearningTimeContent,
testPrep?: LearningTimeTestPrep,
|}>;
/**
* Represents a partially-complete LearningTimeInfo struct.
* This gets propagated down the component hierarchy, accumulating more
* specific information as we know it.
*/
export type PartialLearningTimeInfo = $ReadOnly<{|
...$Rest<
// First, make every field of LearningTimeInfo optional...
Partial<LearningTimeInfo>,
// Then, subtract the `content` and `testPrep` fields...
{|
// NOTE(slim): Using `field: void | T` instead of `field?: T`
// ensures that $Rest<A, B> removes `field` completely from A.
content: void | LearningTimeContent,
testPrep: void | LearningTimeTestPrep,
|},
>,
// Finally, add `content` and `testPrep` back, making all of _their_ fields
// optional.
content?: Partial<LearningTimeContent>,
testPrep?: Partial<LearningTimeTestPrep>,
|}>;
Last active
July 9, 2019 19:18
-
-
Save sliminality/0ab5d8ec0c3ecf90d3f8e6eec9e8e6d4 to your computer and use it in GitHub Desktop.
adventures in Flow typing
function filter<T, P: $Pred<1>>(arr: Array<T>, pred: P): Array<$Refine<T, P, 1>> {
return arr.filter(pred);
}
type A = { which: "A", data: number }
type B = { which: "B", data: string }
type C = { which: "C", data: boolean }
const isA = (x): %checks => x.which === "A";
const isAC = (x): %checks => x.which !== "B";
function filterDemo(arr: Array<A | B | C>) {
(filter(arr, isA): Array<A>);
(filter(arr, isA): Array<B | C>); // errors correctly: B and C have been filtered out
(filter(arr, isAC): Array<A | C>);
(filter(arr, isAC): Array<B>); // errors correctly: B has been filtered out
}
const viewModes = {
default: "default",
compact: "compact",
maximized: "maximized",
};
// tracks refinements -- will error
("aaa": $Keys<typeof viewModes>);
// equivalent to type ViewMode$Values = string;
// doesn't error (unsound)
("aaa": $Values<typeof viewModes>);
Normally, Object.keys()
loses type information:
const viewModes = {
default: "default",
compact: "compact",
maximized: "maximized",
};
const ks = Object.keys(viewModes); // ks has inferred type Array<string>
We can confirm that ks
has inferred type Array<string>
with an equality check:
// The following lines establish type equality between `typeof ks` and `Array<string>`
(ks: Array<string>);
declare var _: Array<string>;
(_: typeof ks);
By casting the call to Object.keys()
to Array<$Keys<typeof viewMode>>
, we retain more specific information about the contents of ks
:
const viewModes = {
default: "default",
compact: "compact",
maximized: "maximized",
};
const ks = (Object.keys(viewModes): Array<$Keys<typeof viewModes>>);
(ks: Array<$Keys<typeof viewModes>>);
declare var _: Array<$Keys<typeof viewModes>>;
(_: typeof ks);
though I'm still not sure what this gets us.
Object.assign()
has a uselessany
type: (source)
/**
* Define a 1:1 mapping between LearningTimeContentKind and
* LearningTimeContentKind$Server.
*
* When you add a new type to enums.ContentKind, you need to add the
* corresponding SCREAMING_SNAKE_CASE type to enums.ContentKind$Server, then
* add both types to this map. The Flow assertions below will statically
* validate these changes.
*
* ACHTUNG(slim): DO NOT annotate the type of this object with an indexer
* property, e.g. {[LearningTimeContentKind]: LearningTimeContentKind$Server} !!
*
* When an object is annotated with an indexer property, Flow will assume that
* all lookups with the correct type are safe, even if the object does not
* include the key. That means it's possible to add a type to the ContentKind
* enum and forget to add it to the map here.
* There is a check below to guard against this.
* https://github.com/facebook/flow/issues/6568#issuecomment-447543153
*/
export const mapContentKindToServer = {
Topic: "TOPIC",
Exercise: "EXERCISE",
Article: "ARTICLE",
Video: "VIDEO",
Challenge: "CHALLENGE",
Talkthrough: "TALKTHROUGH",
Project: "PROJECT",
Interactive: "INTERACTIVE",
TopicQuiz: "TOPIC_QUIZ",
TopicUnitTest: "TOPIC_UNIT_TEST",
};
/**
* ACHTUNG(slim): Modify the code below with extreme care! It verifies the
* following properties of mapContentToServer:
*
* 1. (Soundness) All values in the domain and codomain have the correct type.
* 2. (Completeness) Any time a new key is added to the ContentKind enum, it is
* added to mapContentKindToServer.
*/
// Static check for soundness.
// Ensures every key in `mapContentKindToServer` is a valid ContentKind, that
// is, no superfluous keys are defined.
// This is NOT semantically equivalent to annotating the variable declaration,
// as we will see shortly...
(mapContentKindToServer: {
[LearningTimeContentKind | "__TEST_ONLY"]: LearningTimeContentKind$Server,
});
// An annoying behavior of Flow is that annotating an object declaration with an
// indexer property, e.g. {[K]: T}, will cause ANY lookup with key type K to
// succeed -- even if the key is not actually defined in the object at runtime!
// This is why we instead use the preceding type assertion.
//
// The following assertion ensures that no one accidentally refactors the
// annotation into the declaration.
// It does so by looking up a nonexistent property whose type we added to
// the domain of `mapContentKindToServer`.
// If Flow warns that this suppression is unused, we are in trouble.
// $FlowFixMe
(mapContentKindToServer.__TEST_ONLY: LearningTimeContentKind$Server);
// Static check for completeness.
// This final check ensures that all possible ContentKinds are included in
// `mapContentKindToServer`.
(a: LearningTimeContentKind): LearningTimeContentKind$Server =>
mapContentKindToServer[a];
- SaltyCrane is the best source for Flow types of builtins
- Utility types is the single most useful page in the Flow documentation
- React type reference in the Flow docs
Consider the following code, which behaves as expected:
type Union = "A" | "B" | "C";
declare function randomElement<T>(xs: Array<T>): T;
(randomElement(["A", "B", "C"]): Union);
As expected, attempting to do (randomElement(["A", "B", "D"]): Union);
fails to typecheck:
4: (randomElement(["A", "B", "D"]): Union);
^ Cannot cast `randomElement(...)` to `Union` because string [1] is incompatible with enum [2].
References:
4: (randomElement(["A", "B", "D"]): Union);
^ [1]
4: (randomElement(["A", "B", "D"]): Union);
^ [2]
Suppose we later extend the definition of Union
, adding a "D"
case:
type Union = "A" | "B" | "C" | "D";
Now our array ["A", "B", "C"]
is not exhaustive, and there's no easy solution.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment