What is a generic function in TS and can you write one?
What problem do generics solve?
Without generics, you usually face this choice:
- Be specific → works only for one type.
- Use any → works for all types, but you lose safety.
Generics give you a third option:
"Work for any type, but keep full type safety."
🔁 a generic function is a way to write code that is type-flexible — without using any.
⚙️ meaning: you can provide not only regular arguments, but also typed arguments — and the function still works for different data types.
function myNewFunction<T>(one: T): T {
return one;
}
💡 here, T is the generic type argument. we can call this function like:
const result = myNewFunction<number>(5);
✅ and now result is inferred to be of type number.
🛑 but if you try:
const result = myNewFunction<number>("string");
❌ you get an error — because "string" is not a number, and TS catches that.
📦 that's why they're called generic functions — you declare a type variable like T, and then use it consistently across parameters and return types. this helps you reuse logic, without sacrificing type safety.
❓ what is an as const in typescript? how is it different from normal const?
🛡️ when you use as const, you are telling typescript to treat the entire object as deeply immutable.
🔒 this means you cannot modify any part of it — not even nested properties.
const config = {
host: "whastsf";
port:45 ;
auth: {
client: "ad",
}
} as const;
if you try
config.host = "whatever"; // ❌ not possible
config.auth.client = "whatever"; // ❌ we can not change it
you will get an error.
📦 every nested property becomes immutable.
💬 in js, you could also freeze the object:
Object.freeze(config);
but ⚠️ this only works at runtime — and it only freezes the first level, not the nested ones.
🛠️ as const is evaluated at build time, not runtime - it's a typescript-only tool for locking objects.
What is the difference between type and interface in Typescript? When would you use one or the other?
we could interchange both for most part, the only difference at build time interfaces can be merged, types must be unique.
this means that you can declare the same interface multiple times, and TS will merge them automatically.
but if you declare the same type name twice, it will throw an error.
💡 so in a codebase, you usually go for one of them and stick with it for consistency.
when use them?
if you're building a library, where you want people to extend certain types (e.g. Express.js with Request object), it's better to use an interface — and TS will merge them behind the scenes if a person expands that interface.
i usually use type to define domain entities — e.g. ecommerce Product, Price, etc and use interface for auxiliary shapes like:
- props of a React component
- input params of a function
What is a type guard in TS? When use it
it's a way of writing a function that filters or guards a specific type from a union type.
type Fruit = Pineapple | Banana;
you can write a function like:
function isPineapple(fruit: Fruit): fruit is Pineapple {
return (fruit as Pineapple).spiky !== undefined;
}
this allows you to narrow down the type safely. so inside that condition, TS knows you're working with a Pineapple — and you get autocompletion, type safety, and error checking. use it whenever you're dealing with union types, and you need to work with one specific subtype.
arrays & tuples
arrays
const names: string[] = ["olga", "ana", "maria"];
const ids: Array<number> = [1, 2, 3];
tuples
a tuple is a fixed-length array where each position has a specific type.
const user: [string, number] = ["olga", 25];
// user[0] is string, user[1] is number
const coordinates: [number, number] = [40.7, -74.0];
type HttpResponse = [status: number, body: string];
const res: HttpResponse = [200, '{"ok": true}'];
union types
a union type means a value can be one of several types.
type Status = "loading" | "success" | "error";
type Id = string | number;
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
intersection types
an intersection type combines multiple types into one — the result must satisfy all of them.
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const user: Person = { name: "olga", age: 25 };
literal types
a literal type represents one specific value.
type Direction = "up" | "down" | "left" | "right";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type StatusCode = 200 | 404 | 500;
let dir: Direction = "up"; // ✅
dir = "diagonal"; // ❌ error
optional properties (?)
interface User {
name: string;
email?: string; // optional
}
const user1: User = { name: "olga" }; // ✅
const user2: User = { name: "olga", email: "olga@test.com" }; // ✅
readonly properties
interface Config {
readonly apiUrl: string;
readonly port: number;
}
const config: Config = { apiUrl: "https://api.example.com", port: 3000 };
config.apiUrl = "something"; // ❌ error
index signatures
interface StringMap {
[key: string]: string;
}
const headers: StringMap = {
"Content-Type": "application/json",
"Authorization": "Bearer token123",
};
generic constraints (<T extends Something>)
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("hello"); // ✅
logLength([1, 2, 3]); // ✅
logLength(42); // ❌
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
utility types (extremely common)
Partial<T>
makes all properties optional.
function updateUser(id: string, updates: Partial<User>) {}
updateUser("123", { name: "olga" }); // ✅ only name
Pick<T, Keys> / Omit<T, Keys>
type UserPreview = Pick<User, "name" | "email">;
type UserWithoutAge = Omit<User, "age">;
Record<K, V>
type Role = "admin" | "user" | "guest";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
typing API work
typing request/response
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch("/api/users");
return res.json();
}
function throwError(msg: string): never {
throw new Error(msg);
}
narrowing & guards (external API data)
typeof / instanceof
function process(value: string | number) {
if (typeof value === "string") return value.toUpperCase();
return value * 2;
}
function handleError(error: unknown) {
if (error instanceof Error) console.log(error.message);
}
discriminated unions
type Success = { status: "success"; data: string };
type Failure = { status: "error"; error: string };
type Result = Success | Failure;
function handle(result: Result) {
if (result.status === "success") console.log(result.data);
else console.log(result.error);
}