An interface describes the shape a value must support. A type alias can name an object shape too, and can also express unions and other constructions. Both are erased during compilation. TypeScript is largely structurally typed: compatible members matter more than the name attached to the declaration.
Generics express a relationship that would be lost if every value became unknown. A function returning the first item should preserve the element type of the supplied array and acknowledge that an empty array has no first item.
interface Identified { id: string }
interface Topic extends Identified { title: string }
function first<T>(items: readonly T[]): T | undefined {
return items[0];
}
function indexById<T extends Identified>(items: readonly T[]): Map<string, T> {
const result = new Map<string, T>();
for (const item of items) {
if (result.has(item.id)) throw new Error(`Duplicate id: ${item.id}`);
result.set(item.id, item);
}
return result;
}
const topics: Topic[] = [{ id: "ts", title: "TypeScript" }];
console.log(first(topics)?.title ?? "No topics");
console.log(indexById(topics).get("missing")?.title ?? "Not found");
export {};
The output is TypeScript and Not found. The generic constraint requires an id while preserving other fields of each item. It does not guarantee uniqueness, so the loop still checks duplicate IDs. A Map lookup can be missing even when its declared value type is Topic. Respect the possibly undefined result instead of casting it away.
Design the smallest useful contract
A function needing only id should not demand a whole user profile. Small structural contracts make reuse easier and avoid unnecessary data dependencies. Conversely, a type with many optional properties can allow invalid combinations. Use a union when the valid alternatives differ, and use a complete object type when every field is required together.
Readonly prevents changes through a particular type view; it is shallow and is not runtime freezing. Function parameters can accept readonly arrays so callers do not have to surrender a mutable collection. Copying a container does not copy its objects, so consider whether a returned Map intentionally shares item identities.
Type parameters should correspond to actual relationships. A function that always returns a string generally does not need a generic return parameter. Do not promise callers that they may choose any result type unless the implementation can actually construct it. Prefer inference when it is clear; explicit type arguments are useful when inference lacks enough information.
Understand compatibility limits
Fresh object literals receive useful excess-property checks in many assignment contexts, but structural typing does not generally mean "this object has exactly these keys." Validate external records if unknown properties must be rejected. Likewise, an interface cannot enforce that a duration is positive or a string is a real email address. Use constructors or validators for those runtime invariants.
Exercise: test indexById with two items sharing an id. Add a select(items, predicate) helper that preserves each item's full type and returns an array of matching items without mutating the input.
Solution guidance: write a generic parameter for the item type and a predicate accepting that same type. Use filter or a loop returning a new array. Test empty input, no matches and a custom field beyond id to check that useful type information survives.
References: Object types, generics.
Previous: Unions, Narrowing and Runtime Validation. Next: Type Design, Utility Types and Modules.

