Happylab Logo
Published on

Day22 : Learning TypeScript: Generic Function

Authors

Day22 : Learning TypeScript: Generic Function

The previous article introduced Generics; this one is quite similar XDD, mainly focusing on Generic Functions. If there are any mistakes, please feel free to leave comments. Thank you!


First Experience with Generic Functions

In the following example, we want to get the value of the first element of the input array. We can see that the return type of s is any, but I want to have different types corresponding to the values passed in. How can we do that?

function firstElement(arr: any[]) {
  return arr[0];
}

const s = firstElement(["a", "b", "c"]);
const n = firstElement([1, 2, 3]);

console.log(s); //a
console.log(n); //1

Image

We can use generics. We add <Type> after the function name, where Type is used to represent any input type, allowing type inference to automatically deduce the type. We can see that the variable s will be of type string, while n will be of type number.

function firstElement<Type>(arr: Type[]): Type {
  return arr[0];
}

// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);

console.log(s); //a
console.log(n); //1

Image


Inference

In addition to using <Type>, we can pass multiple parameters like <Input, Output> to let TypeScript automatically infer the types.

In the example below, n is inferred as string, and through parseInt, the string is converted to a number, resulting in parsed being inferred as number[]. The names Input and Output can be defined freely.

function map<Input, Output>(
  arr: Input[],
  func: (arg: Input) => Output
): Output[] {
  return arr.map(func);
}

// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n, 10));

console.log(parsed); //[1,2,3]

Constraints

The following function has two parameters, a and b, and we expect both to have a length property. We can use the extends keyword to constrain the length property and specify that length is of type number.

We can see that only notOK will throw an error because the parameters (10, 100) do not conform to the length property.

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
console.log(longerArray); //[1,2,3]

// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
console.log(longerString); //alice

// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
//error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

For more examples of using generic constraints, refer to this link.


Specifying Type Arguments

In some cases, TypeScript cannot automatically infer the type when using generics, and we can specify it ourselves:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

const arr = combine([1, 2, 3], ["hello"]);
//❌ error: Type 'string' is not assignable to type 'number'.
const arr = combine<string | number>([1, 2, 3], ["hello"]);
console.log(arr); //[1, 2, 3, "hello"]
// ⭕ Specified as a union type string | number.

Writing Good Generic Functions

1. Push Type Parameters Down

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}

//❌ Returns type 'any'
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

2. Use Fewer Type Parameters

Always use as few type parameters as possible.

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}

//❌
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

3. Type Parameters Should Appear Twice

If a type parameter only appears in one location, strongly reconsider if you actually need it.

If only one type is used, we should write it simply.

function greet1<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
greet1("world");

//⭕
function greet2(s: string) {
  console.log("Hello, " + s);
}
greet2("world");

Day22 done. Thank you for reading, see you tomorrow!


References

https://www.typescriptlang.org/docs/handbook/2/functions.html#generic-functions