Practical Typescript snippet

Introducing Useful TypeScript Tips in Real-world

Suyeon Kang
suyeonme

--

Photo by Alex Shuper on Unsplash

Typescript is powerful but the learning curve is low. Going beyond the basics in a complex code base to become a professional TypeScript developer is no easy. However, I’ve realized that certain patterns, based on TypeScript’s syntax, are repeated in various situations. In this article, I’d like to share some TypeScript patterns that I use every day in practice. So, let’s get started.

Section 1: Creating a Union Type with a key or value from an Object

Snippet

// Union Type with keys
type KeyType = keyof typeof OBJECT;

// Union Type with values
type ValueType = typeof OBJECT[keyof typeof OBJECT];

Let’s consider the following personObject.

interface Person {
name: string;
age: number;
job: string;
isMarried: boolean;
};

const personObject: Person = {
name:'Suyeon Kang',
age: 27,
job: 'Web Developer',
isMarried: false
}

You can extract a Union Type with keys or values from the personObject like this.

// Union Type with keys
type PersonKeyType = keyof typeof personObject; // 'name' | 'age' | 'job' | 'isMarried'

// Union Type with values
type PsersonValueType = typeof personObject[KeyType]; // string | number | boolean

Creating KeyType and ValueType every time you need them can be tedious. Instead, you can create a utility type that represents the values of an object using Generics.

// Utility Type
type ValueOf<T> = T[keyof T];

// Usage
type ValueType = ValueOf<Person>;

Consider implementing the library that provides collection of utility types, like utility-types or type-fest if you handle the large scale project.

Section 2: Creating a Union Type from an Array

You can create a union type from an array using as constand typeof ARRAY[number].

Snippet

const ARRAY = [...] as const;
type ArrayValueType = typeof ARRAY[number];

Suppose that you want to create a Union Type with a job list fetched from a server.

const jobs = ["Web Developer", "Chef", "Doctor"] as const;
type JobType = typeof jobs[number]; // "Web Developer" | "Chef" | "Doctor"

Section 3: Creating a String Type including a specific character

Snippet

type NarrowedStringType = `${CHARACTER}${string}`;

This pattern is useful when you want to narrow down a String Type that has too wide scope, like representing hexadecimal color that must start with #.

Hexadecimal Color
type HexColor = `#${string}`; // #${string}
const hexColor_1: HexColor = '#ff0000';
const hexColor_2: HexColor = 'ff0000'; // ERROR!

You can also use Union Types with String Types.

// Example 1
type StringWithPrefix = `${'+' | '-'}${string}`;

// Example 2
type Prefix = '+' | '-';
type StringWithPrefix = `${Prefix}${string}`;

Section 4: Changing the Value Corresponding to a Specific Key of an Object

Snippet

const onChange = <K extends keyof OBJECT>(key: K, value: OBJECT[K]): void => {
onChange(key, value)
};

For front-end developers using React, this pattern might be familiar.

interface Account {
name: string;
company: string;
password: number;
}

const [account, setAccount] = useState<Account>(ACCOUNT);

const onChange = (key: keyof Account, value: Account[keyof Account]): void => {
setAccount(prev => ({ ...prev, [key]: value });
}

It looks good. However, this approach can lead to unexpected behavior. If I pass a value that is a String Type as a parameter to a password field that should be Number Type, the Type checker cannot catch an error.

const onChange = (key: keyof Account, value: Account[keyof Account]): void => {
setAccount(prev => ({ ...prev, [key]: value });
}

onChange('password', 'Suyeon Kang'); // NO ERROR!

To avoid it, narrow the type of the value corresponding to the key.

const onChange = <K extends keyof Account>(key: K, value: Account[K]): void => {
onChange(key, value)
};

onChange('password', 'Suyeon Kang'); // ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.

When K extends keyof T is useful?

K extends keyof Tmeans that K is a subset of keyof T. The pattern like K extends keyof Tis important when you deal with a narrowed union type.

Here is a situation in which we want to only extract a name from an array. name is a String Type, As a result, the extracted result should be Array<string>.

This example is not working. The type of result is (string | number)[]. We have to narrow down the return type.


const EXAMPLE = [
{ name: "Suyeon", age: 27 },
{ name: "Hanna", age: 50 },
];

const extractValueFromArr = <T>(arr: T[], key: keyof T): T[keyof T][] => {
return arr.map((el) => el[key]);
}

const result = extractValueFromArr(ARRAY, "name"); // (string | number)[]

In this situation, K extends keyof T can save us. We can narrow down the return type T[K][] by using generic K. Voilà!

const extractValueFromArr = <T, K extends keyof T>(arr: T[], key: K): T[K][] => {
return arr.map((el) => el[key]);
}

const result = extractValueFromArr(ARRAY, "name"); // string[]

Section 5: Using ReturnType<Type> with Custom Hooks in React

Custom hooks are a great way to separate concerns. You can use ReturnType<Type> to simplify and clarify the code, especially when sharing data with children using Context API.

Let’s suppose that I separate the logic that is related to payment with multiple custom hooks. I want to share this data which is returned from the hooks to children using Context API. In this case, I can add ReturnType<Type> to the types in Context.

Snippet

// usePaymentMethod.ts
const usePaymentMethod = () => {
// more...
return paymentMethodData;
}

// usePaymentAccount.ts
const usePaymentAccount = () => {
// more...
return paymentAccountData;
}

// PaymentContextProvider.tsx
import usePaymentMethodfrom 'usePaymentMethod';
import usePaymentAccountfrom 'usePaymentAccount';

interface PaymentContextType {
paymentData: ReturnType<usePaymentMethod>;
paymentAccountData: ReturnType<usePaymentAccount>;
}

const PaymentContext = createContext<PaymentContextType | null>(null);

const PaymentContextProvider = ({ children }) => {
const paymentMethodData = usePaymentMethod();
const paymentAccountData = usePaymentAccount();

const contextValue = useMemo(() =>
({ paymentMethodData, paymentAccountData })
, [paymentMethodData, paymentAccountData]);

return (
<PaymentContext.Provider value={contextValue}>
{children}
</PaymentContext.Provider>
)
}

Parameters<Type> with Component

Parameters<Type> is also convenient when passing props of Component to a custom hook.

// PaymentMethod.tsx
interface PaymentMethodProps {
method: 'card' | 'cash';
shouldRegisterPayment: boolean;
};

const PaymentMethod = (props: PaymentMethodProps) => {...}

// usePaymentMethod.ts
const usePaymentMethod = (
// Accept the props excluding 'shouldRegisterPayment'
props: Omit<Parameters<typeof PaymentMethod>[0], 'shouldRegisterPayment'>
) => {...}

Let’s look closer at the type in usePaymentMethod.

Parameters<typeof PaymentMethod>[0]

The Parameters<Type> is used to extract the parameter types of a function or constructor. PaymentMethod is a functional component. It means PaymentMethod is a function. So we can access its parameter(props). Parameters<Type>[0] returns a type at the first position in the tuple.

Section 6: Handling Optional Fields as an Object

Handling optional fields related to each other as an object can be more convenient than dealing with them separately. This approach can help you avoid validating each field separately.

Snippet

interface Person {
name: string;
job: string;
rich?: {
hasPrivateYacht: boolean;
runFoundation: boolean;
isHouseOwner: boolean;
isBillionare: boolean;
}
}

const isRichPerson = (person: Person): boolean => {
return person?.rich; // Beautiful
};

If I don’t handle optional fields as an object, it is like following code.

interface Person {
name: string;
job: string;
hasPrivateYacht?: boolean;
runFoundation?: boolean;
}

const isRichPerson = (person: Person): boolean => {
return person?.isHouseOwner && person?.isBillionare; // Validation
};

What if I have more optional fields? It is annoying!

interface Person {
name: string;
job: string;
hasPrivateYacht?: boolean;
runFoundation?: boolean;
isHouseOwner?: boolean;
isBillionare?: boolean;
}

const isRichPerson = (person: Person): boolean => {
return person?.isHouseOwner // Oooops!
&& person?.isBillionare
&& pserson?.isHouseOwner
&& pserson?.isBillionare;
};

Alternative: Tagged Union

Alternatively, you can use a Tagged Union to handle optional fields.

interface Person {
name: string;
job: string;
}

interface RichPerson extends Person {
type: 'rich'; // *
hasPrivateYacht: boolean;
runFoundation: boolean;
isHouseOwner: boolean;
isBillionare: boolean;
}

interface NormalPerson extends Person {
type: 'normal' // *
}

type PersonType = RichPerson | NormalPerson;

const isRichPerson = (person: PersonType): boolean => {
return person.type === 'rich';
};

Conclusion

I hope that these snippets make your daily coding easier. Thank you for reading. Happy Coding!

--

--