Let's think about a simple log function
function log(x) {
console.log(x);
return x;
}
I find it to be more useful than console.log
on its own because I can use it in big chains to see what's going on.
const commandLineArgs = args
.map(validate)
.map(log) // logs values without modifying them
.filter(differentThanDefault)
.map(log)
.reduce(collect, {});
But what's its type signature? As it stands, the function accepts an any
argument and returns an any
value. We do want it to work for any possible value, but by returning an any
we are interfering with TypeScript's ability to track the types of values.
const nums: number[] = [5, 6, 7, 8];
const squares = nums.map(x => x * x); // inferred as number[]
const loggedSquares = log(squares); // inferred as any
This is a problem because if TypeScript thinks our value is of type any
rather than number[]
, it won't catch when we make a mistake:
// Error: Operator '+' cannot be applied to types number[] and 5
const plusFive = squares + 5;
// no complaint from TS
const loggedPlusFive = loggedSquares + 5;
What we really want to say is not "log
accepts an arg of any type and returns a value of any type" but rather "log
accepts an arg of some type and returns a value of that same type". Generic Functions give us a way to do this. Let's rewrite our log
function using generics.
function log<T>(x: T): T {
console.log(x);
return x;
}
The <T>
syntax introduces what's called a "type variable" or "type parameter". Just how function parameters are stand-ins for a value that will be determined later, a type variable is a stand-in for a type that will be determined later. Giving a name to the type is how we're able to specify "log
accepts a variable of some type T
and returns a variable of that same type T
".
Type Parameter Inference
In rare occasions, you may need to specify what concrete types you want in your type parameters. Most of the time, TypeScript will figure it out for you. Using our log
function from before:
const x: number = 5;
log<number>(x); // you're allowed to specify what type you want
log(x); // TS knows x is a number, so it figures out that log<T> should be log<number>
Type parameters in other languages
Sometimes it's helpful to see how the same concept looks in other languages. Python is another language that recently had a type system bolted on top of it. In python, we need to declare a value as a type parameter before using it.
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare the type variable
def first(lst: Sequence[T]) -> T:
return lst[0]
Without that T = TypeVar('T')
declaration, python would go looking for a nearby or imported type called literally T
. Probably, it would fail with a NameError
when it didn't find a type of that name. Worse, maybe there is a type called T
, and we've unwittingly written a function that only works on values of that type. By declaring it as a TypeVar
, we're telling the typechecker: "There isn't really a type called T
. Instead, T
is a placeholder for a type to be decided later.
In my opinion, TypeScript's <T>
is a nicer syntax, but it serves the same purpose.
Multiple Type Parameters
Some functions' type definitions have two or more type variables. map
is a common example: It takes an array of some type (the first type parameter), a function from that first type to another type, and returns an array of that second type. It's even hard to write about without using names! Let's try again with names:
map
accepts an array of some typeT
, a function fromT
to another typeR
, and produces an array ofR
.
With practice, the TypeScript syntax will become easier to read than the english. Here's what it looks like for map:
function map<T, R>(lst: T[], mapper: (t: T) => R): R[]
And once more, with descriptions alongside
function map
<T, R>( // for some types T and R
lst: T[], // lst is an array of T
mapper: (t: T) => R // mapper is a function from T to R
): R[] // The return value is an array of R
Top comments (0)