Constraints
Type params ( <T> ) start out as any until they're given a type (provided by your input values).
Generic constraints allow us to describe the minimum requirement for a type param such that we can achieve a high degree of flexibility while still being able to safely assume some minimal structure and behavior.
REMEMBER: TypeScript is a structural type system.
Constraints provide a minimum requirement of an input type's structure.
If you need an exact requirement then you'd simply demand that specific type and not use a generic since the point of a generic is to provide a level of flexibility.
Example:
A completely flexible generic, where T could be anything:
function listToDict<T>(
  list: T[],
  idGen: (arg: T) => string
): { [k: string]: T } {
  const dict: { [k: string]: T } = {};
  for (let item of list) {
    dict[idGen(item)] = item;
  }
  return dict;
}T extends <Type-Constraint>
What if we wanted to make an assumption about T, or rather demand a minimum requirement of T?
What if every T had to be an object with at least a property id that is of type string?
We could then safely use that id: string as each key [k: string]: T in the response object, and would no longer need the idGen function to generate a key.
interface HasId {
  id: string;
}
interface Dict<T> {
  [k: string]: T;
}
function listToDict<T extends HasId>(list: T[]): Dict<T> {
  const dict: Dict<T> = {};
  list.forEach((item) => {
    dict[item.id] = item;
  });
  return dict;
}T extends HasId guarantees that T is at least a HasId
Scopes
Type params work a similar way as function parameters, in that inner scopes have the ability to access outer scopes but not vice versa:
// outer function
function tupleCreator<T>(first: T) {
  // inner function
  return function finish<S>(last: S): [T, S] {
    return [first, last];
  };
}
const finishTuple = tupleCreator(3);
const t1 = finishTuple(null);
// const t1: [number, null]From the TypeScript Fundamentals, v3↗ course on FEM↗ taught by Mike North↗.