umma.dev

TypeScript: Part Three

Generics in TypeScript can be helpful for building reusable components within applications.

Generics

Building reusable components is important when building applications and this is the purpose of generics.

Identity Function

Without generics, you would have to the function below a specific type as follows:

function Example(arg: number): nunber {
  return arg;
}

You could use any to try to make this function generic but you then lose information about the type the function returns. If you passed in a number, the only informaiton you have is that any type could be returned.

How do you get around this?

You can use a type variable, which is a varaible that works on types rather than values.

function Example<Type>(arg: Type): Type {
  return arg;
}

The Type allows you to capture the type the user provides, so that information can be used later on.

The function above can be called in two ways.

let output = Example<string>("example str");

A more common approach is:

let output = Example("example str");

Generic Type Variables

function myExample<Type>(arg: Type[]): Type[] {
  console.log(arg.length);
  return arg;
}
function myExample<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length);
  return arg;
}

Generic Types

The type of generic functions is similar to those of non-generic functions, with the type parameters listed first.

function Example<Type>(arg: Type): Type {
  return arg;
}

let myExample: <Type>(arg: Type) => Type = Example;

You can use a different name for the generic type parameter in the type, as long as the number of type variables and how the type variables are used line up.

function Example<Type>(arg: Type): Type {
  return arg;
}

let myExample: <Input>(arg: Input) => Input = Example;

This generic type can also be written as a call signature of an object literal type.

function Example<Type>(arg: Type): Type {
  return arg;
}

let myExample: { <Type>(arg: Type): Type } = Example;

Here is a full example of a generic interface.

interface GenericExample<Type> {
  (arg: Type): Type;
}

function Example<Type>(arg: Type): Type {
  return arg;
}

let myExample: GenericExample = Example;

Generic Classes

Generic classes have a generic type parameter list in the angle brackets following the name of the class.

class GenericNumber<NumType> {
  zeroValue: NumType
  add: (x: NumType: y:NumType) => NumType
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
  return x + y
}

Here is another example, using string instead of number.

let strNumeric = new GenericNumber<string>();
strNumeric.zeroValue = "";
strNumeric.add = function (x, y) {
  return x + y;
};

console.log(strNumeric.add(strNumeric.zeroValue, "example"));

Generic Constraints

Create an interface that describes the contraint. Use this interface and the extends keyword to denote the constraint.

interface LengthConstraint {
  length: number;
}

function myExample<Type extends LengthConstraint>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

myExample(3); // error
myExample({ length: 10, value: 5 }); //correct

Using Type Parameters in Generic Constraints

You can declare a type parameter that is contained by another type of parameter.

Example: You would like to get a property from an object given its name. You would like to ensure that you don’t accidentally grab a property that doesn’t exist on obj, so you place a constraint between the two types.

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // no error
getProperty(x, "x"); // error: argument of type "x" is not assignable to the parameter of type "a" | "b" | "c" | "d"

Using Class Types in Generics

When writing factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions.

class BeeKeeper {
  hasMask: boolean = true;
}

class ZooKeeper {
  nametag: string = "name";
}

class Animal {
  numLegs: number = 4;
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag;
createInstance(Bee).keep.hasMask;

Other Examples

function printData<X, Y>(one: X, two: Y) {
  console.log("output: ", one, two);
}

printData("Hello", "World");
printData(1, 2);
printData(123, ["Hello", 123]);
interface UserData<X, Y> {
  name: X;
  age: Y;
}

const user: UserData<string, number> = {
  name: "user",
  age: 10,
};
function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
  let result = {} as Pick<T, K>;
  for (const key of keys) {
    if (key in obj) {
      result[key] = obj[key];
    }
  }
  return result;
}

const lang = {
  name: "TypeScript",
  age: 100,
  extends: ["ts", "tsx"],
};

const ageAndExtensions = pickObjectKeys(language, ["age", "extensions"]);
type User = {
  name: string
}

async function fetchAPI<ResultType = Record<string, any>>(path: string): Promise<ResultType> {
  const res = await fetch(`https://example.com/api${path}`)
  return res.json()
}

const data = await fetchAPI<User[]>('/users/)

export {}
function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
  return Object.keys(obj).reduce(
    (acc, key) => ({
      ...acc,
      [key]: JSON.stringify(obj[key]),
    }),
    {} as { [K in keyof T]: string }
  );
}

const stringifiedValues = stringifyObjectKeyValues({
  a: "1",
  b: 2,
  c: true,
  d: [1, 2, 3],
});
type BooleanFields<T> = {
  [K in keyof T]: boolean;
};

type User = {
  email: string;
  name: string;
};

type UserFetchOptions = BooleanFields<User>;
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
type IsStringType<T> = T extends string ? true : false;

type A = "abc";
type B = {
  name: string;
};

type ResultA = IsStringType<A>;
type ResultB = IsStringType<B>;
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;

functinon someFunction() {
  return true;
}

type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;