As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
TypeScript's type system often feels like a superpower you can learn gradually. I remember when I first started, types were just simple annotations—strings, numbers, and basic interfaces. But over time, I discovered a whole other layer, a set of techniques that let me describe the shape and behavior of my code with incredible precision. These methods catch mistakes before the code runs and make my editor feel intelligent. Today, I want to walk you through some of these powerful methods, from the foundational to the more advanced.
Let's start with conditional types. Think of them as "if statements" for your types. They let a type decide what it is based on another type. This is useful for creating flexible utilities.
type IsNumber<T> = T extends number ? true : false;
type TestA = IsNumber<42>; // true
type TestB = IsNumber<'hello'>; // false
You can use the infer keyword inside a conditional type to "capture" a piece of another type. A classic example is getting the type of value a function returns, even if you don't know the function yet.
type WhatDoesThisReturn<T> = T extends (...args: any) => infer ReturnType ? ReturnType : never;
function fetchData() { return { id: 1, title: 'Item' }; }
type FetchedData = WhatDoesThisReturn<typeof fetchData>;
// FetchedData is now { id: number; title: string; }
Conditional types distribute over unions. This means if you give it string | number, it will apply the logic to string and then to number, and give you back the combined result.
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
Sometimes you don't want that distribution. You can prevent it by wrapping the check in a tuple.
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]
The next concept is mapped types. These allow you to create a new type by transforming each property of an existing type. It's like using map() on an array, but for object keys.
The simplest mapped type might make every property optional or read-only.
type MakeAllOptional<T> = {
[Key in keyof T]?: T[Key];
};
type MakeAllReadonly<T> = {
readonly [Key in keyof T]: T[Key];
};
interface User {
id: number;
name: string;
}
type PartialUser = MakeAllOptional<User>;
// { id?: number; name?: string; }
type ReadonlyUser = MakeAllReadonly<User>;
// { readonly id: number; readonly name: string; }
You can also add or remove modifiers like ? (optional) or readonly using a + or - sign.
type MakeRequired<T> = {
[Key in keyof T]-?: T[Key];
};
type UserWithOptionalId = { id?: number; name: string };
type RequiredUser = MakeRequired<UserWithOptionalId>; // { id: number; name: string }
The real power comes when you combine this with the as clause for key remapping. You can filter properties or rename them based on patterns.
// Only get properties that are functions
type FunctionProperties<T> = {
[Key in keyof T as T[Key] extends Function ? Key : never]: T[Key];
};
// Add a 'get' prefix to every key
type Getters<T> = {
[Key in keyof T as `get${Capitalize<string & Key>}`]: () => T[Key];
};
interface DataModel {
value: number;
calculate(): number;
}
type JustFunctions = FunctionProperties<DataModel>; // { calculate: () => number; }
type DataGetters = Getters<DataModel>;
// { getValue: () => number; getCalculate: () => () => number; }
This leads us directly into template literal types. They let you manipulate string literal types using template string syntax. This is fantastic for creating type-safe routes, CSS class names, or method names.
type Color = 'red' | 'blue';
type Size = 'sm' | 'lg';
type ButtonClass = `btn-${Size}-${Color}`;
// "btn-sm-red" | "btn-sm-blue" | "btn-lg-red" | "btn-lg-blue"
const myClass: ButtonClass = 'btn-lg-red'; // Works
// const badClass: ButtonClass = 'btn-md-green'; // Error
You can also parse these strings back apart using infer within a template literal.
type ExtractParts<T> = T extends `btn-${infer S}-${infer C}` ? [S, C] : never;
type Parts = ExtractParts<'btn-sm-red'>; // ["sm", "red"]
For working with existing values, TypeScript provides powerful inference operators. typeof grabs the static type of a JavaScript value.
const config = { host: 'localhost', port: 3000 };
type Config = typeof config;
// { host: string; port: number; }
The keyof operator gives you a union of an object's keys as string literal types.
interface Product {
id: number;
name: string;
price: number;
}
type ProductKey = keyof Product; // "id" | "name" | "price"
You can combine keyof with indexed access types, which use square brackets to look up the type of a specific property.
type ProductNameType = Product['name']; // string
type ProductPriceType = Product['price']; // number
// You can even look up with a union of keys
type IdOrName = Product['id' | 'name']; // number | string
Indexed access works deeply, which is incredibly useful for extracting types from nested structures without re-declaring them.
interface APIResponse {
data: {
user: {
id: string;
profile: { name: string };
};
};
}
type UserProfileName = APIResponse['data']['user']['profile']['name']; // string
Utility types are pre-built tools that combine these concepts. Pick and Omit are everyday essentials.
interface FullUser {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Create a type for public-facing data
type PublicUser = Pick<FullUser, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string; }
// Or create one by removing sensitive data
type SafeUser = Omit<FullUser, 'passwordHash'>;
// { id: number; name: string; email: string; createdAt: Date; }
Pick and Omit are actually built using mapped and conditional types. Understanding this helps you build your own utilities.
// A simplified version of how 'Pick' might work
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type PickedName = MyPick<FullUser, 'name' | 'email'>;
// { name: string; email: string; }
Sometimes you need to constrain a generic type parameter. This means telling TypeScript, "This type can be anything, as long as it has at least this shape."
// A function that only works with objects that have an 'id' property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const users = [{ id: 'a1', name: 'Alice' }, { id: 'b2', name: 'Bob' }];
const found = getById(users, 'b2'); // Type is { id: string; name: string; }
You can also constrain a type parameter based on another type parameter. This is common in function chains or builders.
// A 'merge' function where the return type is the combination of both inputs
function merge<A extends object, B extends object>(a: A, b: B): A & B {
return { ...a, ...b };
}
const result = merge({ foo: 1 }, { bar: 'two' });
// result has type { foo: number; } & { bar: string; }
Finally, let's talk about recursive types for handling nested structures. You can define a type that references itself. This is perfect for things like tree nodes or nested comments.
interface TreeNode<T> {
value: T;
children?: TreeNode<T>[];
}
const tree: TreeNode<string> = {
value: 'root',
children: [
{ value: 'child1' },
{ value: 'child2', children: [{ value: 'grandchild' }] }
]
};
You can even create recursive type aliases for operations. Here's a type that makes every level of an object deeply optional.
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
interface Settings {
user: {
theme: string;
dashboard: { widgets: string[] };
};
}
type PartialSettings = DeepPartial<Settings>;
// Now user, theme, dashboard, and widgets can all be optional at any depth.
Putting it all together, the goal is to make the type system work for you. It's about describing your intentions so clearly that the compiler can catch inconsistencies. It's not just about preventing errors; it's about creating a living document of your code's contracts.
Start by using typeof and keyof to avoid repetition. Then, try building a small utility type with a conditional or mapped type to solve a specific problem, like extracting all the string properties from an interface. Gradually, these techniques become part of your regular workflow. You'll find yourself modeling your domain not just in your runtime code, but in the type space as well, leading to software that is more robust and easier to change. The compiler becomes a true partner in the development process.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)