Dive deeper than string
and number
and unveil the power of TypeScript’s type system through custom types, generics, and type manipulation. This is where the richness of TypeScript’s type system truly shines, opening doors to more robust and self-explanatory code.
The Building Blocks: Basic Types
I believe you’re familiar with boolean
, string
, number
, and the like. But, a couple of nuances to bear in mind:
Use unknown
as a safer alternative to any
. It forces you to do type checking.
never
isn't just a theoretical type; it's super handy for exhaustive checks.
Interfaces: The Blueprints
Interfaces in TypeScript are like blueprints for objects. Sure, you can use types, but interfaces are more extensible and easier to read. Here’s how you use them to make function contracts:
interface AddFuncSignature {
(a: number, b: number): number;
}
const add: AddFuncSignature = (x, y) => x + y;
Union and Intersection: Playing the Field
Unions |
allow flexibility, while intersections &
enforce that an entity conforms to multiple shapes.
type NetworkLoadingState = {
state: 'loading';
};
type NetworkFailedState = {
state: 'failed';
code: number;
};
type NetworkSuccessState = {
state: 'success';
response: {
title: string;
duration: number;
};
};
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState;
For a more detailed discussion on discriminated unions, check out the Discriminated Unions section in my previous post about TypeScript Pattern 101: Type Guards.
Generics: One Size Doesn’t Fit All
Generics enable you to write code that retains type info throughout execution. A simple example using a generic function:
function firstElement<Type>(arr: Type[]): Type | null {
return arr.length > 0 ? arr[0] : null;
}
const numbers = [1, 2, 3, 4];
const firstNumber = firstElement(numbers); // 1
const strings = ["a", "b", "c"];
const firstString = firstElement(strings); // "a"
Utility Types: Your Type Toolbox
You could write type transformations manually, or you could use utility types:
Partial<T>
makes all properties inT
optional.Readonly<T>
makes all properties inT
read-only.Pick<T, K>
andOmit<T, K>
: For fine-grained object shaping.
Advanced Type Manipulation
Conditional types:
Consider a type that extracts promise value types:
type ExtractPromiseTypeOrDefault<T> = T extends Promise<infer U> ? U : T;
async function fetchData() {
const data: Promise<string> = new Promise((resolve) => resolve("hello world"));
type DataValue = ExtractPromiseTypeOrDefault<typeof data>; // Type is string
const nonPromiseValue: number = 42;
type NonPromiseValueType = ExtractPromiseTypeOrDefault<typeof nonPromiseValue>; // Type is number
}
Here, ExtractPromiseTypeOrDefault
extracts the value type that a promise resolves to. This can be very useful when dealing with asynchronous operations.
Mapped types:
Suppose you’re building a feature flag system to enable or disable certain functionalities in your app.
type FeatureFlags = 'NewDashboard' | 'BetaTesting' | 'DarkMode';
type FeatureConfig = { [K in FeatureFlags]: boolean };
const featureConfig: FeatureConfig = {
NewDashboard: true,
BetaTesting: false,
DarkMode: true,
};
function isFeatureEnabled(flag: FeatureFlags): boolean {
return featureConfig[flag];
}
const isNewDashboardEnabled = isFeatureEnabled('NewDashboard'); // true
Now you have a FeatureConfig
type that maps directly to the state of your features. The isFeatureEnabled
function demonstrates how you might use this mapped type to check the status of a particular feature.
Template literal types:
type EntityType = 'user' | 'post' | 'comment';
type Endpoint = `/${EntityType}/:id`;
function getEndpoint<Entity extends EntityType>(entity: Entity, id: string): Endpoint {
return `/${entity}/${id}` as Endpoint;
}
const userEndpoint = getEndpoint('user', '123'); // Outputs "/user/123"
In this section, the Endpoint
type and getEndpoint
function clearly illustrate the utility of template literal types in constructing URL endpoints in a type-safe manner. Through the example, it's demonstrated how you might utilize template literal types alongside generics to ensure type safety while achieving the desired string output.
The Dark Side of Loose Typing
Using loose types like any
in TypeScript may seem convenient, but it comes with risks, especially in complex systems. It makes your code harder to debug and maintain, undoing some of the benefits of using TypeScript in the first place.
Closing Thoughts
Having discussed about custom types, generics, and advanced type manipulation, we’ve just scratched the surface of what’s possible. The power of TypeScript lies not just in its ability to catch errors at compile time, but in its capability to model the shape of the data within our applications effectively. As we’ve seen, this leads to more self-explanatory, robust, and maintainable code. The examples provided are stepping stones; the real magic happens when you apply these concepts to solve complex problems in your projects. So, take these patterns, extend them, and see how they can elevate your code to a new level of clarity and reliability. Stay tuned for our next topic; catch you then!