Explanation of generics using Javascript Flowtype
I haven't written in a statically typed language before. I am mainly developing in Javascript and lately I have been interested in learning more about FB Flowtype.
I find the documentation nicely written and I understand most of it. However, I don't fully understand the concept of generics . I have tried some examples / explanations but no luck.
Can someone please explain what generics are, what they are mainly used for, and maybe give an example?
source to share
Let's say I want to write a class that just stores one value. Obviously, this is contrived; I keep it simple. In fact, it can be some collection, for example Array
, that can store more than one value.
Let's say I need to wrap number
:
class Wrap {
value: number;
constructor(v: number) {
this.value = v;
}
}
Now I can create an instance that stores a number and I can get this number:
const w = new Wrap(5);
console.log(w.value);
So far so good. But wait, now I also want to wrap string
! If I'm naively just trying to wrap a string, I get the error:
const w = new Wrap("foo");
Gives an error:
const w = new Wrap("foo");
^ string. This type is incompatible with the expected param type of
constructor(v: number) {
^ number
It doesn't work because I told Flow it Wrap
just accepts numbers
. I could rename Wrap
to WrapNumber
, then copy it, call the copy, WrapString
and change number
to string
inside the body. But it's tedious and now I have two copies of the same thing to maintain. If I copy every time I want to wrap a new type, it quickly gets out of hand.
But note that it Wrap
doesn't actually work for value
. It doesn't matter if it is, number
or string
, or something else. It only exists to store it and return it later. The only important invariant here is that the value you give it and the value you return is of the same type . It doesn't matter which particular type is used, it's just that the two values ββare the same.
So, with that in mind, we can add a parameter like:
class Wrap<T> {
value: T;
constructor(v: T) {
this.value = v;
}
}
T
here is just a placeholder. This means, "I don't care what type you specify here, but it's important that T
the same type is used everywhere ." If I pass it to you Wrap<number>
, you can access the property value
and know what it is number
. Likewise, if I pass it to you Wrap<string>
, you know what is value
for this instance - string
. With this new definition for, Wrap
try again to wrap both number
and string
:
function needsNumber(x: number): void {}
function needsString(x: string): void {}
const wNum = new Wrap(5);
const wStr = new Wrap("foo");
needsNumber(wNum.value);
needsString(wStr.value);
Flow describes a type parameter and can figure out that this will all work at runtime. We also get the error as expected if we try to do this:
needsString(wNum.value);
Mistake:
20: needsString(wNum.value);
^ number. This type is incompatible with the expected param type of
11: function needsString(x: string): void {}
^ string
( tryflow for a complete example)
source to share
Generalization between statically typed languages ββis a method of defining a single function or class that can be applied to dependencies of any type, instead of writing a separate function / class for each possible data type. They ensure that the type of one value will always be the same type of another type that is assigned the same general value.
For example, if you wanted to write a function that added two parameters together, that operation (depending on the language) could be completely different. In JavaScript, since it is not a statically typed language, you can do this anyway and type-check inside a function, however Facebook Flow
allows for type consistency and checking in addition to separate definitions.
function add<T>(v1: T, v2: T): T {
if (typeof v1 == 'string')
return `${v1} ${v2}`
else if (typeof v1 == 'object')
return { ...v1, ...v2 }
else
return v1 + v2
}
In this example, we define a function with a common type T
and say that all parameters will be of the same type T
, and the function will always return the same type T
. Inside the function, since we know that the parameters will always be of the same type, we can check the type of one of them using standard JavaScript and return what we perceive and "append" for that type.
When used later in our code, this function can then be called as:
add(2, 3) // 5
add('two', 'three') // 'two three'
add({ two: 2 }, { three: 3 }) // { two: 2, three: 3 }
But we'll throw input errors if we try:
add(2, 'three')
add({ two: 2 }, 3)
// etc.
source to share
Basically, it's just a placeholder for a type.
When using a generic type, we say that any stream type can be used here.
By putting <T>
before the arguments of a function, we say that the function can (but does not have to) use the generic type T
anywhere in its argument list, its body, and as a return type.
Let's take a look at their basic example:
function identity<T>(value: T): T {
return value;
}
This means that the parameter value
inside identity
will have some type that is unknown in advance. Whatever the type, the return value identity
must match that type as well.
const x: string = identity("foo"); // x === "foo"
const y: string = identity(123); // Error
An easy way to think about generics is to imagine one of the primitive types instead T
and see how it works, and then understand that that primitive type can be replaced with any other.
In terms of identity
: think of it as a function that takes [string] and returns [string]. Then understand that [string] can be any other valid stream type. This means that identity
is a function that takes T
and returns a T
, where T
is any stream type.
The docs also have this helpful analogy:
Generic types work the same way as variables or function parameters, except that they are used for types.
Note. Another word for this concept is polymorphism .
source to share