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?
Let'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 incompatible 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 incompatible 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.
Related
I would like to be able to call class prototype methods using bracket notation, so that the method name can be decided at run time:
classInstance['methodName'](arg);
I am failing to do this properly with TypeScript:
class Foo {
readonly ro: string = '';
constructor() {}
fn(s: number) { console.log(s); }
}
const foo = new Foo();
const methods = ['fn'];
foo['fn'](0)
// Type 'undefined' cannot be used as an index type.
foo[methods[0]](1);
// This expression is not callable.
// Not all constituents of type 'string | ((s: number) => void)' are callable.
// Type 'string' has no call signatures.
foo[methods[0] as keyof Foo](1);
The above example is in the TS Playground.
I think that I have a reasonable understanding of what the errors mean and why the string literal in foo['fn'](0) does not produce an error. However, I don't understand how to prevent the errors. I thought that I might be able to use Extract to build a type comprising of Function, but I've failed to do that.
How can I produce a list of typed method names over which my code can iterate? And better, is it possible for the class to export such a list so that users of the class can easily access them?
Background Information
I have a Playwright test that needs to iterate over a list of methods from a Page Object Model, producing a screenshot for each.
When you write
const methods = ['fn'];
The compiler infers the type of methods as string[], which means it may contain any number of any strings at all. So the compiler does not keep track of exactly which values are in the array, or where they are. This allows you to do things later like
methods.push("hello");
Often, this is what people want when they initialize a variable. But in your case, it is a problem, because then methods[0] could be any string whatsoever (or undefined if you have the --noUncheckedIndexedAccess compiler option enabled).
If you want the compiler to keep track of the exact literal types of the values in the array, the easiest way to do so is with a const assertion:
const methods = ['fn'] as const;
This tells the compiler that you would like to treat methods as essentially unchanging, and that it should infer the most specific type it can, more or less. Now methods is inferred to be of type
// const methods: readonly ["fn"]
which means that the compiler knows that methods is a readonly tuple containing exactly one element, whose type is the string literal type "fn".
So now the compiler knows that methods[0] is "fn", and your call compiles with no error:
foo[methods[0]](1); // okay
Playground link to code
You need to make sure that your methods array is typed as an array containing only valid method names:
const methods: ('fn' | …)[] = ['fn'];
Notice that
const methods: (keyof Foo)[] = ['fn'];
doesn't cut it because Foo has also other keys (e.g. ro) that are not the names of methods, or the names of methods with a different signature than you need.
You can also just use
const methods = ['fn'] as const;
For example I have a method isEmpty that checks if anything is empty, null, or undefined and returns true if so.
However in TypeScript it doesn't let the interpreter know that this is the case and I get a red underline in my IDE (WebStorm)
Example code
let str: string | undefined = undefined
if (!isEmpty(str)) {
doSomeWorkFunction(str) // this line is shows the error 'string | undefined is not assignable to type string
}
However if the code is
let str: string | undefined = undefined
if (str) {
doSomeWorkFunction(str) // no error because the interpreter knows I checked the value
}
The fix I would like to avoid is
let str: string | undefined = undefined
if (!isEmpty(str)){
// #ts-ignore
doSomeWorkFunction(str) // no error since ts is now ignoring this error
}
How might I go about still keeping the TypeScript strict null checks in place without having to ignore issues like this.
TypeScript has a feature called "type guards" that helps in this situation: https://www.typescriptlang.org/docs/handbook/advanced-types.html. Specifically, it lets you tell the compiler that the return type is not just a boolean, but a boolean that means something specific about the types of the inputs. For example, you can convert a function like this
function isDefinedString(input: string | undefined): boolean
into a function like this:
function isDefinedString(input: string | undefined): input is string
The return type is still a boolean, but now the compiler will assume that the input is specifically a string and not any other type allowed by the argument declaration (in this case undefined).
Try using this signature on your existing isEmpty function declaration. Although not required to make it work, because you are adding this additional context to the function signature I'd recommend changing the name of isEmpty to reflect its dual purpose of checking emptiness and whether the variable is defined.
Edit:
One caveat to returning type information is that returning false will make the compiler assume that the object is not that type. In the above example, if isDefinedString returns false then the compiler will assume that it is not a string. This runs into problems with any or generic parameters, because returning false effectively tells the compiler that there is no type (or in the compiler's words, there is "never" a type) that satisfies your criteria. While this doesn't result in an error directly, the fact that the compiler has no type that works with your object means you can't do anything meaningful with the object in the if/else branch triggered by your type guard returning false. As such, if you are using a broad type such as any or a generic, you will want to limit what your type guard says to something like input is (null | undefined) or input is MySpecificInterface if you plan to do something meaningful in both true and false cases. This trickiness may also be a sign that you want to separate your validation into two checks:
if(typeGuard(myObject)) {
if(isValid(myObject)) {
// do something with valid object
} else {
// do something with invalid object
}
}
// do nothing without an object to act upon
In TypeScript, there are two distinct "number" types. The first one is called lowercase number, and the second is uppercase Number. If you try printing number, there is compiler error:
console.log(number); //-> error TS2693: 'number' only refers to a type
On the other hand, printing Number will give the standard function description:
console.log(Number); //-> [Function: Number]
This is unsurprising as Number is just a built-in JS constructor documented here. However, it is unclear, what number is supposed to be.
Judging by the error message, it seems that number isn't actually a discrete value(?!) like Number. But despite this, it is used in variable and function declarations as if it is a value, like:
var two: number = 2;
function sqr(x: number) { return x; }
On the other hand, user-defined types like classes appear to be discrete values (as they also print the standard function description). And, to further complicate matters, Number can be used in annotations similar to number:
var two: Number = 2;
There is similar cases with string and String, any and Object, etc.
So, my question is: What are number, string, etc in TypeScript, and how are they different then built-in constructors?
number is only a TypeScript thing - it's a primitive type referring to, well, a number.
But, judging by the error message, it seems that number isn't actually a discrete value like Number.
Indeed - it's a type, so it doesn't exist in emitted code.
a discrete value like Number. On the other hand, user-defined types like classes appear to be discrete values (as they also print the standard function description).
Yes. Classes, of which Number is one, are special. They do two things, somewhat unintuitively:
They create a JavaScript class (usable in emitted code)
They also create an interface for the class (only used by TypeScript)
If you use Number in a place where a type is expected, TypeScript will not complain, because Number is an interface.
If you use Number in a place where a value (something that exists in emitted code) is expected, TypeScript will not complain, because Number is also a global constructor.
In other words, the two Numbers below refer to completely different things:
// refer to the TypeScript Number interface
let foo: Number;
// refer to the JavaScript global.Number constructor
const someNum = Number(someString);
Using Number in TypeScript is very odd, since it'd, strictly, speaking, refer to a number created via new:
const theNum = new Number(6);
Which there's almost never a reason to do. Use a plain primitive number instead, without an object wrapper.
const theNum = 6;
// theNum is typed as `number`
From https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#the-primitives-string-number-and-boolean:
JavaScript has three very commonly used
primitives:
string, number, and boolean. Each has a corresponding type in
TypeScript. As you might expect, these are the same names you’d see if
you used the JavaScript typeof operator on a value of those types:
string represents string values like "Hello, world"
number is for numbers like 42. JavaScript does not have a special runtime value for integers, so there’s no equivalent to int
or float - everything is simply number
boolean is for the two values true and false
The type names String, Number, and Boolean (starting with capital letters) are legal, but refer to some special built-in types
that will very rarely appear in your code. Always use string,
number, or boolean for types.
(There are also typs for null and undefined)
I'm trying to understand type variables for funcitons. The example uses only one but I'm trying to extend it to two. Various inputs produce outputs I don't understand.
function id<T, U>(arg1, arg2: U): U{ // error 1 below
// return arg2 + arg2; // when uncommented, error 2 below
return arg2 * 2
// ^--^ error 3 below
//^-------------^ error 4 below
}
var result = id<string, number>('lorem', 10)
Error 1
Parameter 'arg1' implicitly has an 'any' type.
Error 2
error Operator '+' cannot be applied to types 'U' and 'U'.
Error 3
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
Error 4
Type 'number' is not assignable to type 'U'.
I am confused in a couple of areas, so please feel free to suggest if these should be separate questions:
1: Doesn't specifying argument types with <string, number> register to the called function that these are the types for supplied arguments. Why do these need does : T need to be given again in the parameter list?
2: U here should just be a number, why can't I add it?
3: I don't understand what this error is really driving at.
4: This works if I simply return arg2 without the multiplication. I don't understand why I cannot return a number here.
The first error tells you, that you should always add a type to a parameter. arg1 is currently untyped.
The other errors tell you:
You can only add up (+) numbers and strings, not everything (objects for example). Thats why you have to narrow down the generic U to be either a string or a number.
You can only multiply numbers (*) (and BigInts).
The main point here is: The function has to work without problems in every possible case.
1: Doesn't specifying argument types with register to
the called function that these are the types for supplied arguments.
Why do these need does : T need to be given again in the parameter
list?
The types in the function declaration don't map 1:1 with its parameters. If you want arg1 to be of type U, you'll need to specify it.
2: U here should just be a number, why can't I add it?
3: I don't understand what this error is really driving at.
4: This works if I simply return arg2 without the multiplication. I
don't understand why I cannot return a number here.
Just need to tell TypeScript that U is a number: <T, U extends number>.
Also to note, the compiler can figure out the return type of your function in this case, so you can omit it. I think you want to end up somewhere like this:
function id<T, U extends number>(arg1: T, arg2: U) {
return arg2 * 2;
}
TypeScript is not JS. It is more strict, generally.
TypeScript does not have convention over configuration and you can 3 and more arguments. so you should explicitly set argument type;
There is no operator overloading syntax support in TS (like we have in C#).
Type argument constraints supported, though. So you can write
function id<T, U extends SomeClass>(...)
to use SomeClass props inside you func. And you can write "U extends number" to support add/subtract operations. Also you can use valueOf function of your object (used under the hood by js) and cast arg2 to 'any' (no need to use valueOf if your argument is a number already):
// valid TS code:
var obj = {
foo: 123,
valueOf: function(){return this.foo;}
};
console.log((obj as any) + 1); //will be 124
// workaround - using '+' sign (valueOf used as well)
console.log(+obj + 1); //will be 124
If you want to work with numbers - just work with numbers, or use your specific API (use interfaces)
Here is a simplified example for the Flow errors i'm getting when trying to annotate with generics:
// #flow
function component<T>(state: T) {
let model = deepFreeze(state);
// ^ Cannot call `deepFreeze` with `state` bound to `o`
// because `T` [1] is incompatible with object type [2].
return {
update: (state: T) => {
// etc.
}
};
}
function deepFreeze(o: Object) {
Object.freeze(o);
// etc.
return o;
}
It seems to me that <T> should simply track the type, whatever it is.
In this example i've used the least specific type I could find, about which the docs say: "if you need to opt-out of the type checker, and don’t want to go all the way to any, you can instead use Object".
So how is it possible to use polymorphic types, when they inevitably end up being used in a more specific way elsewhere?
Narrowing the Generic type removes the errors:
function component<T: {}>(state: T) {
...
}
From the docs:
...generics have an “unknown” type. You’re not allowed to use a generic as if it were a specific type.
You could add a type to your generic... This way you can keep the behavior of generics while only allowing certain types to be used.