π§ TypeScript Type Manipulation: Your Magic Toolbox
Imagine you have a magical toolbox. Inside are special tools that can look at any toy, take it apart, and build new toys from the pieces. Thatβs what TypeScriptβs type manipulation tools do with your code!
ποΈ The keyof Operator β Finding All the Keys
Think of an object like a treasure chest with labeled compartments. The keyof operator is like asking: βWhat are all the labels on this chest?β
type Person = {
name: string;
age: number;
email: string;
};
// "What labels does Person have?"
type PersonKeys = keyof Person;
// Result: "name" | "age" | "email"
Why Is This Useful?
When you want to make sure someone only uses real keys from an object:
function getValue<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
const person = { name: "Sam", age: 10 };
getValue(person, "name"); // β
Works!
getValue(person, "color"); // β Error!
π‘ Remember:
keyofgives you a list of all property names as a union type.
π The typeof Operator β Copying a Type from a Value
Sometimes you already have a real thing and want to create a type that matches it exactly. typeof is like taking a photo of something and using that photo as a blueprint!
const robot = {
name: "Beep",
batteryLevel: 100,
isActive: true
};
// Create a type from the robot
type Robot = typeof robot;
// Result: { name: string;
// batteryLevel: number;
// isActive: boolean }
Combining Powers
const colors = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff"
};
type ColorName = keyof typeof colors;
// Result: "red" | "green" | "blue"
π‘ Remember:
typeofextracts a type from an existing value. Itβs like reverse engineering!
π¦ Indexed Access Types β Reaching Inside
Imagine you have a toy box with labeled sections. Indexed access lets you ask: βWhat kind of toy is in the βcarsβ section?β
type Toy = {
cars: string[];
dolls: number;
games: { name: string };
};
type CarsType = Toy["cars"];
// Result: string[]
type GamesType = Toy["games"];
// Result: { name: string }
Going Deeper
You can chain these to dig into nested structures:
type GameName = Toy["games"]["name"];
// Result: string
Using with keyof
type AnyToyType = Toy[keyof Toy];
// Result: string[] | number | { name: string }
π‘ Remember: Use square brackets
[]with a type to look up whatβs inside that property.
π Conditional Types β Making Decisions
What if your type could think and make choices? Conditional types are like asking: βIf this is true, give me A. Otherwise, give me B.β
type IsString<T> = T extends string
? "Yes, it's a string!"
: "Nope, not a string";
type Test1 = IsString<"hello">;
// Result: "Yes, it's a string!"
type Test2 = IsString<42>;
// Result: "Nope, not a string"
Real-World Example
type Flatten<T> = T extends any[]
? T[number]
: T;
type A = Flatten<string[]>;
// Result: string
type B = Flatten<number>;
// Result: number
π‘ Remember:
T extends U ? X : Ymeans βIs T a kind of U? If yes, use X. If no, use Y.β
π£ The infer Keyword β Catching Types
The infer keyword is like a fishing net that catches types as they pass by. You use it inside conditional types to extract a piece of a type.
type GetReturnType<T> = T extends
(...args: any[]) => infer R
? R
: never;
type A = GetReturnType<() => string>;
// Result: string
type B = GetReturnType<() => number[]>;
// Result: number[]
Another Example: Array Elements
type ElementOf<T> = T extends (infer E)[]
? E
: never;
type X = ElementOf<string[]>;
// Result: string
type Y = ElementOf<number[]>;
// Result: number
π‘ Remember:
infercreates a temporary variable to capture part of a type during pattern matching.
π Distributive Conditional Types β One at a Time
When you pass a union type to a conditional type, something magical happens. TypeScript checks each member of the union separately!
type ToArray<T> = T extends any
? T[]
: never;
type Result = ToArray<string | number>;
// Checks: string β string[]
// Checks: number β number[]
// Result: string[] | number[]
Distribution in Action
graph TD A["ToArray<string | number>"] --> B["string | number"] B --> C["Check: string"] B --> D["Check: number"] C --> E["string[]"] D --> F["number[]"] E --> G["string[] | number[]"] F --> G
Preventing Distribution
Sometimes you donβt want this behavior. Wrap in brackets:
type ToArrayNoDistribute<T> = [T] extends [any]
? T[]
: never;
type Result = ToArrayNoDistribute<string | number>;
// Result: (string | number)[]
π‘ Remember: Conditional types distribute over unions by default. Use
[T]to prevent this.
π Recursive Conditional Types β Types That Call Themselves
What if a type needs to keep working on itself, layer by layer? Like peeling an onion π§ !
type DeepFlatten<T> = T extends any[]
? DeepFlatten<T[number]>
: T;
type A = DeepFlatten<string[]>;
// Result: string
type B = DeepFlatten<number[][]>;
// Result: number
type C = DeepFlatten<boolean[][][]>;
// Result: boolean
Building Deep Readonly
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Original = {
a: { b: { c: string } }
};
type Frozen = DeepReadonly<Original>;
// All levels are now readonly!
π‘ Remember: Recursive types reference themselves to handle nested structures.
π‘οΈ The NoInfer Utility Type β Stopping Inference
Sometimes TypeScript tries too hard to guess types. NoInfer tells TypeScript: βDonβt guess here! Use what I already told you.β
function createPair<T>(
value: T,
defaultValue: NoInfer<T>
) {
return { value, defaultValue };
}
// Without NoInfer, TS might get confused
createPair("hello", "world"); // β
Both strings
// This helps prevent accidental widening
createPair(1, 2); // β
Both numbers
When to Use It
Use NoInfer when you have multiple parameters of the same type and want one of them to not influence how TypeScript guesses the type.
function choose<T>(
items: T[],
fallback: NoInfer<T>
): T {
return items[0] ?? fallback;
}
choose([1, 2, 3], 0); // β
T is number
choose(["a", "b"], "default"); // β
T is string
π‘ Remember:
NoInfer<T>makes that parameter βinvisibleβ during type inference.
π― Quick Reference Table
| Tool | What It Does | Example |
|---|---|---|
keyof |
Gets all property names | keyof Person β "name" | "age" |
typeof |
Extracts type from value | typeof obj β { a: number } |
T["key"] |
Looks up property type | Person["name"] β string |
T extends U ? X : Y |
Makes decisions | IsString<5> β "no" |
infer |
Captures types | GetReturn<fn> β return type |
| Distributive | Checks union members separately | ToArray<A | B> β A[] | B[] |
| Recursive | Types calling themselves | DeepFlatten<T[][]> |
NoInfer |
Prevents inference | fn(x: NoInfer<T>) |
π Putting It All Together
Hereβs a real example using multiple tools:
type API = {
getUser: (id: number) => { name: string };
getPosts: () => { title: string }[];
};
// Get all method names
type Methods = keyof API;
// "getUser" | "getPosts"
// Get return type of getUser
type UserResponse = ReturnType<API["getUser"]>;
// { name: string }
// Flatten the posts array
type Post = API["getPosts"] extends
() => (infer P)[] ? P : never;
// { title: string }
π You Did It!
You now have a magical toolbox with:
- ποΈ keyof β to find all the keys
- π typeof β to copy types from values
- π¦ Indexed Access β to reach inside
- π Conditional Types β to make decisions
- π£ infer β to catch hidden types
- π Distributive Types β to work on unions one by one
- π Recursive Types β to handle deep nesting
- π‘οΈ NoInfer β to control inference
With these tools, you can transform, reshape, and create new types like magic! πͺ
