Contents
In brief
Generics are useful only while their inputs are meaningfully constrained. A helper that accepts an arbitrary object and an arbitrary string key cannot prove that the requested property exists, so a seemingly harmless mistake can surface only after deployment.
The Dev.to article examines the combination of extends, keyof, and indexed access T[K]. Together, they turn assumptions about data shape into compiler-checked contracts and reject invalid property reads or writes before the program runs.
What happened
The starting point is a familiar helper such as get<T>(obj: T, key: string). It looks flexible, but key can be any text and T promises no properties at all; the type checker has no basis for a safe lookup.
A constraint such as T extends SomeType declares the minimum shape a caller must provide. With T extends { id: string }, the implementation can safely use item.id, while a number or an object without id is rejected.
The keyof operator derives a union of valid property names. For a User containing id and name, it produces "id" | "name" and stays current when that type changes.
The common pattern combines both ideas:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
K is now proven to be a key of T, and T[K] preserves the exact type of the selected property. Reading id returns a number; reading name returns a string.
Why it matters
Without this connection, a misspelled field can compile successfully and later return undefined or fail far from the original call. Forms, configuration objects, and API clients are particularly exposed because field names are commonly passed as strings.
Compile-time feedback moves the failure to the authoring moment. getProperty(user, "age") is highlighted immediately when age is absent, rather than becoming a production investigation.
The same precision protects updates. In updateProperty<T, K extends keyof T>(obj, key, value: T[K]), the value must match the chosen field: true cannot be stored in a numeric port.
This is not a replacement for runtime validation of network input. TypeScript describes JavaScript ahead of execution; untrusted JSON still needs schema validation or explicit checks at the boundary.
In practice
First identify the properties a helper actually needs. A function that reads an identifier should use T extends { id: number }, not require a complete User shape containing unrelated fields.
For a property selected by name, use the paired parameters T and K extends keyof T. The pattern works well in form utilities, state helpers, query builders, and other code that must retain the relationship between a key and its value.
- Model optional fields honestly:
email?belongs tokeyof User, butT["email"]can beundefined. - With index signatures, narrow to
K extends keyof T & stringwhen the API accepts string keys only. - Avoid broad constraints such as
T extends objectwhen a function relies on named properties. - Validate external JSON separately, then pass verified values into strictly typed helpers.
Conditional and mapped types build on the same rules. { [K in keyof T]: T[K] | null } transforms every property while retaining its keys, and T extends U ? X : Y selects a result type from a condition.
Takeaway
extends and keyof are not ceremony: they record the smallest contract a function needs and preserve the concrete result type. Keep <T, K extends keyof T> close for code that reads or updates fields by name.
The useful balance is a constraint that is specific enough to protect the operation but no stricter than necessary. Then a typo or configuration mismatch becomes a compiler message rather than a user-facing failure.

