TypeScript type implementation

5 TypeScript God-tier Tricks You MUST Know

TypeScript is more powerful than just interfaces

Alfredo Ryelcius
Published in
5 min readMar 31, 2023

--

When I first encountered TypeScript, I have to admit, I wasn’t a fan. As a JavaScript developer, the idea of a strictly-typed programming language seemed like an unnecessary hassle. However, after diving into TypeScript and exploring its advanced features and capabilities, I realized that this language has so much to offer.

But this meme sums my experience up:

source: https://www.reddit.com/r/ProgrammerHumor/comments/lntpto/low_effort_meme_inspired_by_my_friends_experience/

Working with it for a few years now, I can confidently say that this is where I want to be. It is powerful, safe, and flexible. Its type system, interfaces, and classes provide a level of structure and reliability that can greatly improve the maintainability of your code.

But, as with any new tool, there is a learning curve involved. I try to present them in an easy-to-digest, real-world applicable scenario.

Typed return value according to parameter

type GetUsernameAdditionalArgs = { required?: boolean };
type GetUsernameReturnValue<T extends GetUsernameAdditionalArgs> =
T["required"] extends true ? string : string | null;

function getUsername<T extends GetUsernameAdditionalArgs>(
id: number,
additionalArgs?: T
): GetUsernameReturnValue<T> {
// get username
const username = "...";

if (additionalArgs?.required && username == null) {
throw Error("");
}

return username as GetUsernameReturnValue<T>;
}

// username will be of type string | null
const username = getUsername(10);

// requiredUsername will be of type string
const requiredUsername = getUsername(10, { required: true });

This is a very useful skill to apply type annotations to functions that may return a null or undefined value. For example, if you have a function called getUsername that fetches data from an external API, you can add an if block to throw an error if the username is required.

The best part is that you can also disable the try-catch block as needed, giving you more control over the function's behavior.

Extend interface to make fields optional

interface User {
username: string;
userId: number;
description?: string;
}

interface UserWithOptionalUsername extends Omit<User, 'username'> {
username?: string;
}

const user: UserWithOptionalUsername = {
userId: 10,
};

Sometimes, we may need to make certain fields optional in our TypeScript interfaces. To achieve this, we can extend the Omit utility type from our base interface and redefine the field with a question mark (?) to make it optional.

Extend interface to make fields required

interface User {
username: string;
userId: number;
description?: string;
}

interface UserWithDescription extends User {
description: string;
}

const userWithDescription: UserWithDescription = {
userId: 10,
username: "",
description: "",
};

In some scenarios, we may need to make certain properties required in our TypeScript interfaces. To achieve this, we can extend the base interface and redefine the property without the question mark (?) to make it required.

Using namespaces in TypeScript

namespace MyService {
export function findUser() {}
}

const user = MyService.findUser();

namespace in TypeScript is not widely used because we can simply use named imports:

import * as Module from '.'

console.log(Module.myVar);

However, namespace can provide an easy and strict way to organize code into logical groupings, unlike module.

Another benefit of using namespaces is that they can help to encapsulate code and prevent it from leaking into the global namespace. This can help prevent issues with global namespace pollution, which can lead to bugs and conflicts between different parts of your code.

Typed switch-case without default values

// initial code
type Status = "running" | "pending" | "success";

function execByStatus(status: Status): number {
switch (status) {
case "running":
return 1;
case "pending":
return 2;
case "success":
return 3;
}
}

// =================================================================== //
// modified Status type
type Status = "running" | "pending" | "success" | "stuck";

// will give the error: Function lacks ending return statement and
// return type does not include 'undefined'
// error catched during compilation!!
function execByStatus(status: Status): number {
switch (status) {
case "running":
return 1;
case "pending":
return 2;
case "success":
return 3;
}
}

In JavaScript, it’s common to use a default case in switch statements to handle unexpected code changes. However, in TypeScript, it’s often beneficial to omit the default case altogether.

By not providing a default case, we can enforce a stricter configuration of the switch statement to specifically handle each individual case. If additional cases are added in the future, the TypeScript compiler will fail during compilation, alerting us to the new case and allowing us to correctly handle it.

Overall, this approach can help to catch errors and make our code more robust, since we can be confident that each case is being handled explicitly and thoroughly.

Extra: TypeScript Required, Pick, Omit, and Partial

Here are some scenarios where you might want to implement some of the most powerful types in TypeScript:

Base interface

// Base interface 
interface User {
email: string;
username: string;
description: string;
}

Required<T>

// All properties in User is now required 
type UserWithDescription = Required<User>;
const userWithDescription: UserWithDescription = {
email: "",
username: "",
description: "",
};

Required<T> is a built-in utility type in TypeScript that creates a new type by making all properties of the original type T required.

Pick<T>

// Only pick a few properties from base interface
type UserWithoutDescriptionV1 = Pick<User, "email" | "username">;
const userWithDescriptionV1: UserWithoutDescriptionV1 = {
email: "",
username: "",
};

Pick<T, K> is a built-in utility type in TypeScript that creates a new type by picking a set of properties K from the original type T. Use | to add more keys.

Omit<T>

// Only pick a few properties from base interface
type UserWithoutDescriptionV2 = Omit<User, "description">;
const userWithDescriptionV2: UserWithoutDescriptionV2 = {
email: "",
username: "",
};

Omit<T, K> is a built-in utility type in TypeScript that creates a new type by omitting a set of properties K from the original type T.

Partial<T>

type IncompleteUser = Partial<User>;
const incompleteUser: IncompleteUser = {};

Partial<T> is a built-in utility type in TypeScript that creates a new type by making all properties of the original type T optional.

These TypeScript tricks are definitely useful for your projects. Let me know if you want me to write about anything else related to TypeScript. Until next time!

--

--

Alfredo Ryelcius
Writer for

TypeScript Geek during the day 👨🏻‍💻 Aspiring Writer at night ✍🏻 Writing about programming or maybe life lessons 🤷‍♂️