Smart Generic Functions
A practical path to modeling generic functions in TypeScript without losing API clarity.
- typescript
- frontend
- code-quality
In a recent refactor I needed to return only name and age from a User without losing the link to the original type. The first version worked, and it was throwaway code.
The version you don't reuse
type User = {
id: string;
name: string;
age: number;
email: string;
}
function getNameAndAge(user: User): { name: string; age: number } {
return {
name: user.name,
age: user.age,
};
}
const user: User = {
id: '1234',
name: 'William',
age: 36,
email: 'iwilldev@outlook.com.br'
}
const userNameAndAge = getNameAndAge(user);
// valor: { "name": "William", "age": 36 }
// tipo: { name: string, age: number }
The return value is a new object with no relationship to User. If email becomes required in another shape, you rewrite the function or invent another type.
Generic + inference: a pick that holds up
function pick<T, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const userNameAndAge = pick(user, ["name", "age"])
// valor: { "name": "William", "age": 36 }
// tipo: Pick<User, "name" | "age">
TypeScript knows which keys you asked for. Autocomplete and compile errors follow.
You can extend in the same direction
omit: remove keys and keep the rest typed:
type OmitKeys<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
function omit<T, K extends keyof T>(
obj: T,
keys: K[]
): OmitKeys<T, K> {
const result = { ...obj };
keys.forEach(key => delete result[key]);
return result;
}
const userWithoutId = omit(user, ["id"])
// tipo: OmitKeys<User, "id">
merge: combine two objects without losing either type:
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
type Role = {
role: 'admin' | 'editor' | 'viewer';
permissions: Array<'read' | 'write' | 'delete' | 'update'>;
};
const role: Role = {
role: 'viewer',
permissions: ['read']
}
const userWithRole = merge(user, role)
// tipo: User & Role
Lodash and similar libraries already ship this. I reach for local helpers when the scope is small and I want zero extra dependencies, with types that reflect exactly what the project needs.