I have never written in statically typed language before. I'm mostly developing in Javascript and lately I've been interested in learning more about FB's Flowtype.
I find the documentation nicely written and I understand most of it. However I don't quite get the concept of generics. I've tried googling some examples / explanations but with no luck.
Could someone please explain what generics are, what are they mostly used for and perhaps provide an example?
I have never written in statically typed language before. I'm mostly developing in Javascript and lately I've been interested in learning more about FB's Flowtype.
I find the documentation nicely written and I understand most of it. However I don't quite get the concept of generics. I've tried googling some examples / explanations but with no luck.
Could someone please explain what generics are, what are they mostly used for and perhaps provide an example?
Share Improve this question asked Jun 28, 2017 at 18:07 user3056783user3056783 2,6443 gold badges36 silver badges63 bronze badges3 Answers
Reset to default 6Let's say I want to write a class that just stores a single value. Obviously this is contrived; I'm keeping it simple. In reality this might be some collection, like an Array
, that can store more than one value.
Let's say I need to wrap a 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 that number out:
const w = new Wrap(5);
console.log(w.value);
So far so good. But wait, now I also want to wrap a string
! If I naively just try to wrap a string, I get an error:
const w = new Wrap("foo");
Gives the error:
const w = new Wrap("foo");
^ string. This type is inpatible with the expected param type of
constructor(v: number) {
^ number
This doesn't work because I told Flow that Wrap
just takes numbers
. I could rename Wrap
to WrapNumber
, then copy it, call the copy WrapString
, and change number
to string
inside the body. But that is tedious and now I have two copies of the same thing to maintain. If I keep copying every time I want to wrap a new type, this will quickly get out of hand.
But notice that Wrap
doesn't actually operate on the value
. It doesn't care whether it is number
or string
, or something else. It only exists to store it and give it back later. The only important invariant here is that the value you give it and the value you get back are the same type. It doesn't matter what specific type is used, just that those two values have the same one.
So, with that in mind we can add a type parameter:
class Wrap<T> {
value: T;
constructor(v: T) {
this.value = v;
}
}
T
here is just a placeholder. It means "I don't care what type you put here, but it's important that everywhere T
is used, it is the same type." If I pass you a Wrap<number>
you can access the value
property and know that it is a number
. Similarly, if I pass you a Wrap<string>
you know that the value
for that instance is a string
. With this new definition for Wrap
, let's try again to wrap both a number
and a 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 infers the type parameter and is able to understand that everything here will work at runtime. We also get an error, as expected, if we try to do this:
needsString(wNum.value);
Error:
20: needsString(wNum.value);
^ number. This type is inpatible with the expected param type of
11: function needsString(x: string): void {}
^ string
(tryflow for the full example)
Generics among statically typed languages are a method of defining a single function or class that can be applied to any type dependency 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 at the type of another that are assigned to the same generic value.
For example, if you wanted to write a function that added two parameters together, that operation (depending on the language) could be entirely different. In JavaScript, since it is not a statically typed language to begin with, you can do this anyway and type check within the function, however Facebook's Flow
allows for type consistency and validation in addition to single 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 generic 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 of the function since we know that the parameters will always be of the same type, we can test the type of one of them using standard JavaScript and return what we perceive and "addition" for that type to be.
When in use 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 will throw typing errors if we attempt:
add(2, 'three')
add({ two: 2 }, 3)
// etc.
Basically, it's just a placeholder for a type.
When using a generic type, we are saying that any Flow type can be used here instead.
By putting <T>
before the function arguments, we're saying that this function can (but doesn't have to) use a generic type T
anywhere within its arguments list, its body, and as its return type.
Let's look at their basic example:
function identity<T>(value: T): T {
return value;
}
This means that the parameter value
within identity
will have some type, which isn't known in advance. Whatever that type is, the return value of 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 of T
and see how that would work, then understand that this primitive type can be substituted for any other.
In terms of identity
: think of it as a function that accepts a [string] and returns a [string]. Then understand that [string] can be any other valid flow type as well.
This means identity
is a function that accepts T
and returns a T
, where T
is any flow type.
The docs also have this helpful analogy:
Generic types work a lot like variables or function parameters except that they are used for types.
Note: Another word for this concept is polymorphism.