Recommended resources
The freely available resources this lesson will direct you to.
Hello. In the previous lesson, you gave projects and events precise TypeScript shapes, then narrowed unions before using their properties. Those types describe particular values. Today you’ll write functions that work across different shapes without losing what is specific about each one.
The central question is: what must a function know about its input to do its job, and what information should its return type preserve? Allow about 40 minutes for the lesson, video, and readings.
Preserve a relationship with a type parameter
Consider a function that returns its argument. You could make it accept any, but then TypeScript would forget the return value’s type:
function keepAny(value: any): any {
return value;
}
const result = keepAny({ id: "p1", title: "Portfolio" });
// result is any: a misspelled property may go unnoticed
A generic type parameter captures the input type and uses it again for the output:
function keep<Value>(value: Value): Value {
return value;
}
const result = keep({ id: "p1", title: "Portfolio" });
// result retains its object type
Value is not a particular type chosen when you write keep. TypeScript usually infers it separately at each call. The useful part is not that the function accepts many types; any does that too. It is the promise that the returned value has the same type as the input.
Read the TypeScript Handbook’s Generics introduction for the identity-function version of this distinction.
Read the official TypeScript Handbook’s introduction to generics to see how a type parameter retains information that any loses.
In “Hello World of Generics,” begin with the identity example. Continue through the calls with explicit and inferred type arguments, stopping before “Working with Generic Type Variables.” Focus on why the input and output use the same type parameter.
An explicit call such as keep<string>("hello") is possible, but inference makes keep("hello") sufficient. Prefer inference when the arguments already provide the information TypeScript needs.
Constrain a type only when the function needs a capability
An unconstrained type parameter could be a number, string, object, or something else. Consequently, this function cannot assume that its input has an id:
function getId<Item>(item: Item): string {
return item.id; // Error: Item is not known to have an id
}
A constraint states the minimum shape the function requires:
function getId<Item extends { id: string }>(item: Item): string {
return item.id;
}
Here, extends means that Item must have an id of type string. It does not mean that Item is reduced to { id: string }; it may have additional properties.
Read the Handbook’s “Generic Constraints” subsection to see how a required property makes an operation legal inside a generic function.
In “Generic Constraints,” start at the length example. Read the following valid object call as well, then stop before “Using Type Parameters in Generic Constraints.” Compare the compiler’s objection to unconstrained .length with the calls the constrained version accepts and rejects.
For an application collection, the same idea gives you a reusable lookup. Assume the Project interface from the previous lesson is available:
interface Task {
id: string;
done: boolean;
}
function findById<Item extends { id: string }>(
items: readonly Item[],
id: string
): Item | undefined {
return items.find((item) => item.id === id);
}
declare const projects: Project[];
declare const tasks: Task[];
const project = findById(projects, "p1"); // Project | undefined
const task = findById(tasks, "t1"); // Task | undefined
The constraint permits item.id inside the function. The type parameter preserves the element type for callers: looking up a task does not erase its done property, and looking up a project does not erase its title. The undefined is equally important. A valid ID type does not guarantee that an item with that ID exists, so callers must handle a failed lookup.
A constraint describes a minimum, not a complete replacement for the input type. Suppose a function promises to return Item but, on one branch, constructs only { length: 6 }. That object meets a { length: number } constraint, yet it might lack properties that the caller’s actual Item has. Read the Handbook’s concrete example of this mistake:
TypeScript: Documentation - More on Functions
Read this subsection of the official Handbook to distinguish “satisfies the constraint” from “is the particular input type.”
In “Working with Constrained Values,” begin at the minimumLength function declaration and read the explanation and failure through the array’s slice call. Stop before “Specifying Type Arguments.” Pay attention to why returning an object with length cannot fulfill a promise to return the caller’s specific type.
One safe design is to return the original value when it qualifies and undefined otherwise. Like findById, that signature preserves the specific input type without claiming to create a new value of that type.
Relate a key to its object
The next constraint relates two inputs. A property-access helper should accept different objects, but its key must belong to the particular object supplied.
Watch Angular University’s Typescript Generics – Understanding the keyof Operator. The video develops the key relationship from an unsafe helper; afterward, we’ll refine its return type explicitly.
💥 Typescript Generics - Understanding the keyof Operator
Watch “Typescript Generics – Understanding the keyof Operator” from Angular University to see how an object type determines its allowed property keys.
Watch valid keys for the meaning of keyof, then unsafe access for the problem the helper needs to solve. Continue through generic extraction, focusing on how the object and key type parameters are inferred and how an invalid key is rejected.
For an object type with id and title, keyof produces the available key types. A second type parameter can then be constrained to those keys:
function getProperty<Obj extends object, Key extends keyof Obj>(
obj: Obj,
key: Key
): Obj[Key] {
return obj[key];
}
Read the signature in parts. Obj extends object limits this helper to objects. Key extends keyof Obj limits the key to a property of that object type. Finally, Obj[Key] is the type of the property selected by that key. The body performs an ordinary JavaScript property lookup; these type relationships are compile-time checks.
The provided image shows a similar helper correctly rejecting an invalid key:

There is a useful improvement over the pictured code: its return type is explicitly unknown, even for "age". Using Obj[Key] retains the connection between the chosen key and its value:
declare const project: Project;
const title = getProperty(project, "title"); // string
const description = getProperty(project, "description"); // string | undefined
getProperty(project, "missing"); // Type error: not a Project key
"description" is a valid key, but its value may be absent because the property is optional. Conversely, "missing" is not a key declared on Project, so TypeScript rejects that call. These checks concern values TypeScript knows about; they do not validate an object received from an untrusted source.
Keep the generic signature as small as the job
Generics are useful when a type parameter expresses a relationship. In findById, Item connects the array’s element type to the return type. In getProperty, Obj and Key connect the object, its valid keys, and the selected value. A parameter that adds no relationship often adds complexity without helping callers.
For example, this does not need to be generic:
function labelId<Id extends string>(id: Id): string {
return `ID: ${id}`;
}
Id does not affect the return type or relate multiple arguments. function labelId(id: string): string says what the function needs more directly. Constraints should also be no broader than necessary: findById needs an id, not an entire Project.
The Handbook collects practical guidance on this point:
TypeScript: Documentation - More on Functions
Read the official Handbook’s guidance to help decide when a type parameter or constraint earns its place.
In “Guidelines for Writing Good Generic Functions,” begin with the three guidelines. Read the “Push Type Parameters Down,” “Use Fewer Type Parameters,” and “Type Parameters Should Appear Twice” subsections, then stop before “Optional Parameters.” Treat these as design checks, not rigid rules: compare them with the relationships expressed by findById and getProperty.
Key takeaways
A generic function is valuable when it preserves a type relationship across its inputs and output. Add an extends constraint to state what the function must be able to use, while remembering that the caller’s actual type may contain more. For object property helpers, Key extends keyof Obj rejects invalid keys and Obj[Key] preserves the selected value’s type. Keep signatures narrow enough to be useful without making callers manage unnecessary type parameters.
Next, you’ll put these typed functions into a strict TypeScript Node.js project, where the compiler and npm scripts can check them consistently.