Skip to content

REUSABLE TYPES · LESSON 9 OF 24

Generics and constraints

Preserve relationships between input and output types.

  • type parameters
  • constraints
  • inference
  • readonly

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.

1. Understand the concepts

A generic function describes a relationship that works for many types. first<T> returns the element type of the array it receives. Returning unknown would lose that relationship; returning any would disable checking.

T is chosen for each call, usually through inference. It is not a runtime class and cannot be used to construct a value or test instanceof T. Types disappear, so runtime behavior still needs actual values or callbacks.

A constraint limits what T may be. T extends { id: number } permits reading id while preserving additional fields. It does not say T is exactly that minimal shape.

Keep generic parameters useful. A parameter appearing only once may not express a relationship and can often be replaced by a simpler type. Return undefined explicitly when an empty input is possible.

2. Build and run

These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.

src/main.ts

src/main.ts
function first<T>(values: readonly T[]): T | undefined {
  return values[0];
}
function findById<T extends { id: number }>(values: readonly T[], id: number): T | undefined {
  return values.find(value => value.id === id);
}
const students = [{ id: 1, name: "Asha" }, { id: 2, name: "Ravi" }];
console.log(first([10, 20]));
console.log(findById(students, 2)?.name);
console.log(first<string>([]) ?? "Empty");

package.json

package.json
{
  "name": "edupoly-typescript-generics-and-constraints",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "check": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "types": [],
    "moduleDetection": "force"
  },
  "include": [
    "src/**/*"
  ]
}

3. Trace the code and check the result

  1. first infers number from the first call.
  2. findById keeps the name property in its inferred return type.
  3. The explicit string argument is useful when the input array is empty.

Expected result

10
Ravi
Empty

4. Practice independently

Write last<T> with the same empty-array behavior.

Show the explanation

Return values[values.length - 1] from function last<T>(values: readonly T[]): T | undefined. Do not assert that an item exists.

When something goes wrong

If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.

Read the related official TypeScript documentation ↗