TypeScript From Zero

Functions and Generics

Function types, overloads, this parameters, generics and constraints, a typed fetch wrapper and a typed event emitter.

Lesson 0324 min read#typescript#generics#functions

On this page

  1. Words used in this lesson
  2. Function types
  3. Parameters: optional, default and rest
  4. Overloads
  5. this in functions
  6. Generics: one function, many types
  7. keyof, typeof and indexed access
  8. A typed fetch wrapper
  9. A typed event emitter
  10. Try it
  11. Interview questions
  12. Key takeaways
  13. What's next

In one line: a function type says what goes in and what comes out. A generic is a function (or type) with a placeholder, written <T>, that TypeScript fills in from the arguments, so one definition keeps the exact types for every use. Learn to read a signature, to add a constraint (T extends ...), and to know when you do not need a generic at all.

Lesson 2 described objects. This lesson is about functions, which is where most real TypeScript lives: parameters, return types, overloads, the this problem, and then generics, which let one function work on many types without giving up checking. We end with two small useful tools, a typed fetch wrapper and a typed event emitter. Start at Lesson 0 if you have no project yet. The interview versions are in Full-Stack Q&A, Lesson 3 and Frontend Depth, Lesson 4.

What I ran / checked. Everything here was compiled with TypeScript 7.0.2 (tsc --pretty false) on Node v22.22.0, using the tsconfig from Lesson 0. Programs that print something were run with node file.ts, and the output is quoted. The fetch wrapper ran against a small HTTP server on 127.0.0.1 that I started in the same script, so no real website was involved, and the port number in the one message that shows it changes on every run. Error messages are copied from the compiler. The <const T> example needs a recent TypeScript; I only know it works on 7.0.2. I did not read the handbook for this lesson. The "reveal the type" trick (const x: never = value) is the one from Lesson 1.

Words used in this lesson

  • Signature is the part of a function that is its type: the parameters and the return type, without the body.
  • Optional parameter (x?: string) may be left out when calling. Default parameter (x = "!") gets a value if left out. Rest parameter (...nums: number[]) collects the extra arguments into an array.
  • void is the return type of a function whose result you do not use. never is the type of a function that never returns normally.
  • Overload is a function with several allowed call shapes, listed before one shared implementation.
  • this parameter is a fake first parameter, named this, that only tells the checker what this must be. It is erased.
  • Generic is a function, type or class with one or more type parameters (placeholders such as T).
  • Type argument is the real type that fills the placeholder: in Box<number>, number is the type argument.
  • Constraint (T extends X) limits which types may fill the placeholder.
  • Inference is TypeScript choosing the type arguments for you from the call.
  • keyof T is the union of the property names of T. typeof x (in a type) is the type of the value x. T[K] (indexed access) is the type of property K on T.

Function types

You can write the type of a function on its own, with an arrow:

JavaScript
type BinaryOp = (a: number, b: number) => number;const add: BinaryOp = (a, b) => a + b;const bad: BinaryOp = (a, b) => String(a + b);const fewer: BinaryOp = (a) => a;const more: BinaryOp = (a, b, c) => a + b + c;
TEXT
src/g1.ts(3,33): error TS2322: Type 'string' is not assignable to type 'number'.src/g1.ts(5,7): error TS2322: Type '(a: any, b: any, c: any) => any' is not assignable to type 'BinaryOp'.  Target signature provides too few arguments. Expected 3 or more, but got 2.src/g1.ts(5,25): error TS7006: Parameter 'a' implicitly has an 'any' type.src/g1.ts(5,28): error TS7006: Parameter 'b' implicitly has an 'any' type.src/g1.ts(5,31): error TS7006: Parameter 'c' implicitly has an 'any' type.

Notice four things:

  • In add, I wrote no types on a and b. They come from BinaryOp. This is called contextual typing: the place where a function is used tells TypeScript the parameter types. It is why [1, 2].map((n) => n * 2) needs no note on n.
  • bad returned text where a number was required.
  • fewer is fine: a function that takes fewer parameters can be used where more are passed, because JavaScript ignores extra arguments. That is what makes callbacks like arr.map((x) => x) work, with map passing three arguments.
  • more asks for more than the type gives, so it is an error. Also a, b, c became any, because there was no type for c to come from (the TS7006 lines, same as in Lesson 0).

Parameters: optional, default and rest

JavaScript
function greet(name: string, greeting?: string, punct = "!") {  return (greeting ?? "Hello") + ", " + name + punct;}console.log(greet("Ada"), greet("Bo", "Hi"), greet("Cy", undefined, "?"));greet();greet("a", "b", "c", "d");function sum(label: string, ...nums: number[]) {  return label + nums.reduce((a, b) => a + b, 0);}console.log(sum("t=", 1, 2, 3));sum("t=", 1, "2");const args: [string, number, number] = ["x", 1, 2];console.log(sum(...args));
TEXT
Hello, Ada! Hi, Bo! Hello, Cy?t=6x3src/g1.ts(11,1): error TS2554: Expected 1-3 arguments, but got 0.src/g1.ts(12,22): error TS2554: Expected 1-3 arguments, but got 4.src/g1.ts(18,14): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

(The three lines of program output came from running a copy of the file without the failing lines; the tsc lines came from the file above.)

  • greeting?: string has the type string | undefined inside the function. punct = "!" has the type string; the default is the type. A default parameter is also optional for the caller, so the message says "1-3 arguments".
  • A required parameter cannot follow an optional one. Put optional and default parameters last.
  • ...nums: number[] collects the rest. A call with a wrong type in the rest is checked item by item.
  • A tuple type lets you spread an array into fixed positions: sum(...args) works because args has exactly the right shape (Lesson 1).

void and never

JavaScript
function fail(msg: string): never { throw new Error(msg); }function log(msg: string): void { console.log(msg); }const cb: () => void = () => 42;const val = cb();const probe: string = val;
TEXT
src/g1.ts(27,7): error TS2322: Type 'void' is not assignable to type 'string'.

void means "do not use the result". A function type that returns void accepts a function that returns something (() => 42 was accepted), because the caller is promised not to look at the result. This is why arr.forEach((x) => list.push(x)) is fine even though push returns a number. never is for a function that always throws or never ends.

Overloads

Sometimes the result type depends on the argument type in a way a union cannot show. You write overload signatures (just the shape, no body), then one implementation that accepts everything:

JavaScript
function make(tag: "a"): string;function make(tag: "b"): number;function make(tag: "a" | "b"): string | number {  return tag === "a" ? "text" : 1;}const x = make("a");const y = make("b");const px: never = x;const py: never = y;
TEXT
src/g2.ts(17,7): error TS2322: Type 'string' is not assignable to type 'never'.src/g2.ts(18,7): error TS2322: Type 'number' is not assignable to type 'never'.

make("a") is a string and make("b") is a number; a single union return type could not say that. The implementation signature is not visible to callers. Only the overloads count:

JavaScript
function len(x: string): number;function len(x: unknown[]): number;function len(x: string | unknown[]): number {  return x.length;}console.log(len("abc"), len([1, 2]));len(42);len(Math.random() > 0.5 ? "a" : [1]);
TEXT
3 2src/g2.ts(7,5): error TS2769: No overload matches this call.  The last overload gave the following error.    Argument of type 'number' is not assignable to parameter of type 'unknown[]'.src/g2.ts(8,5): error TS2769: No overload matches this call.  The last overload gave the following error.    Argument of type '"a" | number[]' is not assignable to parameter of type 'unknown[]'.      Type 'string' is not assignable to type 'unknown[]'.

The second error is the useful lesson. The argument was string | number[], and the implementation accepts that, but neither overload does, so the call fails. Overloads are matched one at a time and each must accept the whole argument.

Prefer a union or a generic when you can. function len(x: string | unknown[]): number has no overloads and accepts all of these calls. Use overloads only when the return type really depends on the argument. A union parameter plus a generic often does the same job with less code.

this in functions

In JavaScript, this depends on how a function is called, not where it was written. TypeScript lets you state what a function expects, with a fake first parameter named this, which is removed from the output:

JavaScript
type Handler = { label: string; run(this: { label: string }): string };const h: Handler = { label: "hi", run() { return this.label; } };console.log(h.run());const detached = h.run;detached();function who(this: { name: string }) { return this.name; }who();console.log(who.call({ name: "Ada" }));
TEXT
src/g3.ts(5,1): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ label: string; }'.src/g3.ts(8,1): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ name: string; }'.

Both bad calls (detached() and who()) are caught at compile time. Without the this parameter, tsc printed nothing for the same detached call in a class method (Lesson 2: it failed at runtime with TypeError: Cannot read properties of undefined (reading 'label')). Running the first file without the failing lines, h.run() printed hi. Arrow functions have no this of their own; they use the one around them. That is why an arrow-function field in a class is safe to pass around.

Generics: one function, many types

Write a function that returns the first item of a list. Without generics, you must choose: any[] (no checking) or one function per type.

JavaScript
function first<T>(items: T[]): T | undefined {  return items[0];}const a = first([1, 2, 3]);const b = first(["x"]);const c = first<boolean>([]);

I revealed the types of a, b and c with never:

TEXT
src/h1.ts(7,7): error TS2322: Type 'number | undefined' is not assignable to type 'never'.  Type 'undefined' is not assignable to type 'never'.src/h1.ts(8,7): error TS2322: Type 'string | undefined' is not assignable to type 'never'.  Type 'undefined' is not assignable to type 'never'.src/h1.ts(9,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'never'.  Type 'undefined' is not assignable to type 'never'.

<T> is a variable for a type. For first([1, 2, 3]), TypeScript looked at the argument (number[]) and decided T = number. This is inference, and most of the time you do not write the type argument yourself. In first<boolean>([]) I did write it, because an empty list gives nothing to infer from.

Read function first<T>(items: T[]): T | undefined aloud: "for any type T, take a list of T and give back a T or undefined". The link between input and output is the point. The type does not get lost on the way through.

Inference follows the arguments

JavaScript
function identity<T>(x: T): T { return x; }const i1 = identity("hi");let i2 = identity("hi");
TEXT
src/h1.ts(14,7): error TS2322: Type '"hi"' is not assignable to type 'never'.src/h1.ts(15,7): error TS2322: Type 'string' is not assignable to type 'never'.

i1 is the literal type "hi" (a const), and i2 is widened to string (a let), the same rule as Lesson 1.

Inference also works from a callback. TypeScript fills T from the first argument and U from what the callback returns:

JavaScript
function mapIt<T, U>(xs: T[], f: (x: T) => U): U[] {  const out: U[] = [];  for (const x of xs) out.push(f(x));  return out;}const lens = mapIt(["a", "bb"], (s) => s.length);mapIt([1, 2], (n) => n.toUpperCase());
TEXT
src/o1.ts(7,7): error TS2322: Type 'number[]' is not assignable to type 'never'.src/o1.ts(8,24): error TS2339: Property 'toUpperCase' does not exist on type 'number'.

s was a string with no note from me, lens is number[], and in the second call n is known to be a number. (The first error line is my reveal of lens.)

When one T is used for two parameters, the first one wins, and the second must fit:

JavaScript
function same<T>(a: T, b: T): T { return a; }same(1, "a");same<number>(1, "a");
TEXT
src/o1.ts(12,9): error TS2345: Argument of type '"a"' is not assignable to parameter of type '1'.src/o1.ts(13,17): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

In the first call TypeScript took T from 1 (so the message says 1), and "a" does not fit. If you want it to accept both, you need two type parameters or a union.

Constraints: T extends ...

A bare T can be anything, so you can do almost nothing with it:

JavaScript
function wrong<T>(x: T) { return x.toUpperCase(); }
TEXT
src/h1.ts(36,36): error TS2339: Property 'toUpperCase' does not exist on type 'T'.

Say what you need from T with extends:

JavaScript
function longest<T extends { length: number }>(a: T, b: T): T {  return a.length >= b.length ? a : b;}console.log(longest("abc", "de"), longest([1, 2], [1]));longest(10, 20);
TEXT
abc [ 1, 2 ]src/h1.ts(21,9): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Strings and arrays both have a length, so longest works for both and returns the same type that went in (a string for strings, a number[] for arrays). A function that took { length: number } and returned { length: number } would lose that. Numbers have no length, so longest(10, 20) is refused.

A generic must earn its place. If T appears only once in a signature, it links nothing. function log<T>(x: T): void is the same as function log(x: unknown): void. A good question to ask: "does T connect an input to an output, or one input to another?" If not, remove it.

Default type parameters

A type parameter can have a default, like a default argument:

JavaScript
interface Result<T, E = Error> { ok: boolean; data?: T; error?: E }const r1: Result<string> = { ok: true, data: "x" };const r2: Result<string, string> = { ok: false, error: new Error("no") };
TEXT
src/i1.ts(6,49): error TS2322: Type 'Error' is not assignable to type 'string'.

Result<string> means Result<string, Error>. In r2 I gave E as string, so an Error object no longer fits. A default makes a generic easy to use in the common case. You need to provide the type arguments if there is no default and nothing to infer from: const b2: Box = { value: 1 } fails with TS2314: Generic type 'Box<T>' requires 1 type argument(s).

Generic interfaces, types and classes

Everything above works for types as well:

JavaScript
type Stack<T> = { items: T[]; push(x: T): void; pop(): T | undefined };function makeStack<T>(): Stack<T> {  const items: T[] = [];  return { items, push: (x) => { items.push(x); }, pop: () => items.pop() };}const s = makeStack<number>();s.push(1);s.push("2");
TEXT
src/i1.ts(15,8): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Built-in types are generic too: Array<T>, Map<K, V>, Promise<T>. I checked that Promise.resolve(5) has the type Promise<number>, and that new Map<string, number>().set("a", "b") fails on the "b".

A class can have type parameters, with a constraint and a default:

JavaScript
class Cache<K extends string | number, V = unknown> {  private data = new Map<K, V>();  get(k: K): V | undefined { return this.data.get(k); }  set(k: K, v: V): this { this.data.set(k, v); return this; }}const c = new Cache<string, number>().set("a", 1);new Cache<boolean>();
TEXT
src/i1.ts(31,11): error TS2344: Type 'boolean' does not satisfy the constraint 'string | number'.

A const type parameter

A recent TypeScript feature: <const T> asks inference to keep literal types, as if the argument had as const (Lesson 2).

JavaScript
function ident<const T>(x: T): T { return x; }const k1 = ident(["a", "b"]);const k2 = ident({ mode: "dark" });
TEXT
src/o1.ts(27,7): error TS2322: Type 'readonly ["a", "b"]' is not assignable to type 'never'.src/o1.ts(28,7): error TS2322: Type '{ readonly mode: "dark"; }' is not assignable to type 'never'.

Without const, k1 would be string[]. I use this when a function takes a config or a list of names and the exact words matter.

keyof, typeof and indexed access

Three small operators are the base for most advanced types (Lesson 4):

JavaScript
const user = { id: 1, name: "Ada", tags: ["a", "b"] };type User = typeof user;type Keys = keyof User;type NameType = User["name"];type TagItem = User["tags"][number];const k: Keys = "email";type Roles = { admin: "A"; guest: "G" };type R = Roles[keyof Roles];const r: R = "X";type Arr = string[];type El = Arr[number];const e: El = 5;
TEXT
src/j1.ts(7,7): error TS2322: Type '"email"' is not assignable to type '"id" | "name" | "tags"'.src/j1.ts(12,7): error TS2322: Type '"X"' is not assignable to type 'R'.src/j1.ts(28,7): error TS2322: Type 'number' is not assignable to type 'string'.
  • typeof user (in a type position) takes the type from a value. User is { id: number; name: string; tags: string[] } and you did not write it twice.
  • keyof User is "id" | "name" | "tags", as the first error shows.
  • User["name"] is string. User["tags"][number] is string (the type of an item in the list). Roles[keyof Roles] is the union of all the values, "A" | "G".
  • Arr[number] is the item type of an array, and e: El = 5 fails with string as the target.

They combine into the most useful generic pattern: a key that must belong to the object:

JavaScript
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {  return obj[key];}const nm = getProp(user, "name");getProp(user, "age");
TEXT
src/h1.ts(30,15): error TS2345: Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'.

(In that file user had only id and name.) The result nm has the type string: K was inferred as "name", and T[K] is T["name"]. A typo in the key is a compile error, and the result type is exact. I also ran a pick(user, "id", "name") that returns Pick<T, K>; it printed { id: 1, name: 'Ada' }. Pick and the other built-in helpers are the subject of Lesson 4.

A typed fetch wrapper

fetch(...).then((r) => r.json()) gives any. The tempting wrapper puts a generic around it:

JavaScript
async function getJson<T>(url: string): Promise<T> {  const res = await fetch(url);  if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);  return (await res.json()) as T;}

This compiles and is wrong. T is chosen by the caller and never checked. It is the same lie as the as in Lesson 0: the generic only moves the cast into the function. A better design makes the caller supply a check, a parser that turns unknown into T:

JavaScript
type Parser<T> = (raw: unknown) => T;async function fetchJson<T>(url: string, parse: Parser<T>): Promise<T> {  const res = await fetch(url);  if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);  const raw: unknown = await res.json();  return parse(raw);}type Todo = { id: number; title: string; done: boolean };function parseTodo(raw: unknown): Todo {  if (typeof raw !== "object" || raw === null) throw new Error("not an object");  const o = raw as Record<string, unknown>;  if (typeof o.id !== "number") throw new Error("id must be a number");  if (typeof o.title !== "string") throw new Error("title must be a string");  if (typeof o.done !== "boolean") throw new Error("done must be a boolean");  return { id: o.id, title: o.title, done: o.done };}

Now T is inferred from the parser's return type, and the data is checked before anyone treats it as a Todo. The cast inside parseTodo (raw as Record<string, unknown>) is safe because I checked typeof raw === "object" and not null first, and every field is checked after. A third step: return errors as values and not as exceptions, using the discriminated union from Lesson 1:

JavaScript
type Ok<T> = { ok: true; value: T };type Err = { ok: false; error: string };async function tryFetch<T>(url: string, parse: Parser<T>): Promise<Ok<T> | Err> {  try {    return { ok: true, value: await fetchJson(url, parse) };  } catch (e) {    return { ok: false, error: e instanceof Error ? e.message : String(e) };  }}

I ran all three against a local server that returns a good Todo at /todo/1, a bad one (id is the text "1", no done) at /todo/bad, and a 404 for anything else:

TEXT
getJson says id is a number, runtime says: stringfetchJson: WRITE false/todo/1 ok write/todo/bad error: id must be a number/missing error: HTTP 404 for http://127.0.0.1:42019/missing

The first line is the problem with getJson<Todo>: the type said number and the data said string, and tsc printed no error. With tryFetch, the caller has to check r.ok before touching r.value, because the union is narrowed on ok.

Writing a parser by hand for every type is long. In Lesson 6 we use a library (Zod) that builds the parser from a schema and gives you the type from the same schema.

A typed event emitter

An event emitter lets code say "when X happens, call me". The common bug is that the event name and the data do not match. We can make the compiler check both, with one map from event name to payload type:

JavaScript
type Handler<P> = (payload: P) => void;class Emitter<Events extends Record<string, unknown>> {  private handlers: { [K in keyof Events]?: Handler<Events[K]>[] } = {};  on<K extends keyof Events>(event: K, fn: Handler<Events[K]>): () => void {    const list = (this.handlers[event] ??= []);    list.push(fn);    return () => {      const i = list.indexOf(fn);      if (i >= 0) list.splice(i, 1);    };  }  emit<K extends keyof Events>(event: K, payload: Events[K]): void {    for (const fn of this.handlers[event] ?? []) fn(payload);  }}

The pieces are the ones from this lesson: K extends keyof Events makes the name one of the known events, and Events[K] is the payload type for that name. The { [K in keyof Events]?: ... } line is a mapped type (Lesson 4); here it just says "for each event name, an optional list of handlers". on returns a function that removes the handler. Use it:

JavaScript
type AppEvents = {  login: { user: string };  logout: undefined;  error: Error;};const bus = new Emitter<AppEvents>();const off = bus.on("login", (p) => console.log("hello", p.user.toUpperCase()));bus.on("error", (e) => console.log("error:", e.message));bus.on("logout", () => console.log("bye"));bus.emit("login", { user: "ada" });bus.emit("error", new Error("disk full"));bus.emit("logout", undefined);off();bus.emit("login", { user: "nobody sees this" });console.log("done");
TEXT
hello ADAerror: disk fullbyedone

p has the type { user: string } without a note from me, and after off() the last emit did nothing. Now the mistakes. I used a declaration of the class here to keep the file short (declare class has no body), and the checks are the same:

JavaScript
bus.emit("logn", { user: "a" });bus.emit("login", { usr: "a" });bus.emit("login", "ada");bus.on("error", (e) => e.user);bus.on("login", (p: number) => {});
TEXT
src/l3.ts(8,10): error TS2345: Argument of type '"logn"' is not assignable to parameter of type 'keyof AppEvents'.src/l3.ts(9,21): error TS2561: Object literal may only specify known properties, but 'usr' does not exist in type '{ user: string; }'. Did you mean to write 'user'?src/l3.ts(10,19): error TS2345: Argument of type 'string' is not assignable to parameter of type '{ user: string; }'.src/l3.ts(11,26): error TS2339: Property 'user' does not exist on type 'Error'.src/l3.ts(12,17): error TS2345: Argument of type '(p: number) => void' is not assignable to parameter of type 'Handler<{ user: string; }>'.  Types of parameters 'p' and 'payload' are incompatible.    Type '{ user: string; }' is not assignable to type 'number'.

A wrong event name, a typo in the payload (with the "did you mean" hint), the wrong payload type, a wrong use inside a handler and a handler with the wrong parameter type: all five are caught. One rough edge: an event with no data is typed undefined and you must still pass undefined to emit. A nicer API can be built with tuples of parameters; I did not try it here.

Try it

  1. Write function last<T>(xs: T[]): T | undefined. Call it with [1, 2] and with []. What is the type of the result in each case?
  2. Write function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]. Call it on a list of users with "name", then with "nmae". What happens?
  3. Write function merge<T extends object, U extends object>(a: T, b: U): T & U using { ...a, ...b }. What is the result type for merge({ a: 1 }, { b: "x" })?
  4. Write function describe(x: string): string and function describe(x: number): number as overloads. Then pass string | number. What does the compiler say, and how would you fix it without a third overload?
  5. Take getJson<T> above and rewrite it so that the caller must pass a parser. Run it against a bad response. What changes at runtime?

Answers.

  1. For [1, 2] the result is number | undefined. For [] it is undefined (I revealed both with the never trick: Type 'undefined' is not assignable to type 'never'), because an empty list gives nothing to infer T from.
  2. The first call returns string[]. The second fails with TS2345: Argument of type '"nmae"' is not assignable to parameter of type 'keyof User'.
  3. The result type is { a: number; } & { b: string; }, and at runtime merge printed { a: 1, b: 'x' }.
  4. The call fails with TS2769: No overload matches this call (the last overload says Argument of type 'string | number' is not assignable to parameter of type 'number'). The fix is one signature: function describe<T extends string | number>(x: T): T; I ran describe2(5) and describe2("a") and got 5 a.
  5. fetchJson(url, parseTodo) throws id must be a number for the bad response, and the program stops at the real place of the problem, instead of crashing later in unrelated code.

Interview questions

What is a generic and why do we use it? (Must know)

A generic is a function, type or class with a type parameter, a placeholder like T. It lets one definition work for many types and keeps the link between them: first<T>(items: T[]): T | undefined returns a number for a list of numbers and a string for a list of strings. The alternative is any, which loses that link, or a copy of the function for every type.

How does TypeScript infer type arguments? (Must know)

It looks at the arguments of the call and finds a type for each type parameter. For first([1, 2, 3]) it sees number[] and sets T = number. It also infers from callbacks: in mapIt(["a", "bb"], (s) => s.length), T came from the list and U from what the callback returned. When there is nothing to infer from, such as an empty list, I give the type argument myself: first<boolean>([]). If one T is used in two parameters, the first argument sets it, and the second must fit (same(1, "a") failed with parameter of type '1').

What is a constraint and when do you need one? (Must know)

A constraint, T extends X, limits what T can be, so I can use X's members inside the function. A bare T has no members: x.toUpperCase() gave TS2339 ... does not exist on type 'T'. With T extends { length: number }, strings and arrays are accepted, numbers are refused, and the function returns the same type it was given.

What does K extends keyof T give you? (Senior signal)

It says "K must be one of the property names of T". With the return type T[K], getProp(user, "name") returns string, and a wrong key such as "age" is a compile error that lists the allowed keys. It is the pattern behind safe property access, pick, and my typed event emitter (on<K extends keyof Events>).

When is a generic a bad idea? (Senior signal)

When the type parameter appears only once in the signature, so it connects nothing: log<T>(x: T) is just log(x: unknown). A generic that returns a constraint's type loses the caller's type; a generic that links input to output keeps it. I ask: "does T link two places?" If not, I remove it. Extra type parameters make error messages longer too.

Why is getJson<T>(url): Promise<T> a trap? (Must know)

T is chosen by the caller and nothing checks it, so it is a cast hidden inside a function. In my test, getJson<Todo> returned data whose id was a string while the type said number, and there was no compile error. I make the caller pass a parser, (raw: unknown) => T, so that T is inferred from it and the data is checked before it is typed. In real projects I would generate the parser from a schema with a library like Zod (Lesson 6).

How do overloads work, and what is hidden? (Senior signal)

I write several signatures with no body, then one implementation that accepts all of them. The caller only sees the overloads, never the implementation signature. They are tried in order and each must accept the whole argument. In my test len(Math.random() > 0.5 ? "a" : [1]) failed with TS2769: No overload matches this call, although the implementation accepted string | unknown[]. If a union or a generic does the same job, I prefer that.

What is the this parameter? (Senior signal)

A fake first parameter named this that tells the checker what this must be when the function is called. It is erased in the output. function who(this: { name: string }) refused who() with TS2684, and who.call({ name: "Ada" }) worked. It catches the "method lost its object" bug, for example const f = obj.method; f(). Arrow functions take this from where they are written, so they do not need it.

What is the difference between void and never? (Must know)

void is "returns, but ignore the result". A function type returning void can be given a function that returns something (const cb: () => void = () => 42 compiled), which is why forEach callbacks can return values. never is "never returns": it throws, loops forever, or is a value that cannot exist. const val = cb() has the type void, and assigning it to a string failed.

Explain keyof, typeof and indexed access with one example. (Must know)

For const user = { id: 1, name: "Ada", tags: ["a", "b"] }: typeof user is the object's type; keyof typeof user is "id" | "name" | "tags"; (typeof user)["name"] is string; and (typeof user)["tags"][number] is string, the item type. I use typeof so I do not write a type twice, and keyof to limit a key to real property names.

How would you type an event emitter so that event names and payloads must match? (Senior signal)

I keep one map type, type AppEvents = { login: { user: string }; error: Error }, and make the class generic over it. on<K extends keyof Events>(event: K, fn: (payload: Events[K]) => void) and emit<K extends keyof Events>(event: K, payload: Events[K]) tie the name to the payload. In my run, a wrong name, a misspelt payload field, a wrong payload type and a handler with the wrong parameter all failed at compile time, and the handler p was typed with no note from me.

What does a default type parameter do? (Must know)

It is used when the caller does not give that type argument and nothing can be inferred, like a default function argument. interface Result<T, E = Error> means Result<string> is Result<string, Error>. Without a default, Box with no argument gave TS2314: Generic type 'Box<T>' requires 1 type argument(s). I add defaults to make the common case short.

Key takeaways

  • A function type is (params) => result. Parameter types come from context when a callback is typed by its place.
  • Optional and default parameters go last; rest parameters collect extra arguments. void means ignore the result; never means it never returns.
  • Overloads show only their signatures to callers; each must accept the whole argument. A union or generic is often simpler.
  • A this parameter makes the checker catch lost-this calls, and it is erased.
  • A generic <T> links types between input and output. TypeScript infers it from the arguments and callbacks.
  • Add extends when you need members of T; use K extends keyof T with T[K] for safe keys; remove a T that connects nothing.
  • A generic fetch wrapper that only casts is a lie. Make the caller pass a parser so that T is checked.
  • One map from event name to payload makes an event emitter safe to use.

What's next

Lesson 4: Utility, Mapped and Conditional Types. We use the built-in helpers (Partial, Pick, Omit, Record, ReturnType), then build our own with mapped and conditional types, which is what the { [K in keyof Events]?: ... } line in the emitter was a first look at. Practise first: change Emitter so that emit for an event with undefined payload needs only one argument. Later lessons: Lesson 5 on modules and tsconfig, Lesson 6 on React, Node and Zod, and Lesson 7 on reading errors and migrating to strict mode.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series