Sai Umesh

Unlocking Conditional Types, Generic Functions, and Enum Key Magic

9 min read

In this article, I’ll walk you through some of the coolest TypeScript features like conditional types, generic functions, and how to extract keys from arrays and enums. These techniques can really level up your code by making it more flexible and safer to use. You’ll see how TypeScript has your back with catching errors and adapting function inputs based on what you pass in. We’ll break it down with simple, practical examples, showing you how to avoid those pesky mistakes and write cleaner, more reliable code!

Extracting Keys from an Enum

Imagine you have an enum and want to extract the keys as types. TypeScript can help you easily do that! Let’s take a look

export enum WeekDays {
  Monday,
  Tuesday,
}
// WeekDaysKey will now be: "Monday" | "Tuesday"
export type WeekDaysKey = keyof typeof WeekDays;

Explanation

• We define an enum WeekDays with two values: Monday and Tuesday
• Using keyof typeof WeekDays, we can extract the keys (“Monday” and “Tuesday”) and turn them into a union type
• The result is WeekDaysKey, which will be “Monday” | “Tuesday”. Now you can use this type wherever you need the enum keys as a type

Extracting a type from an Array

Let’s say you want to take an array and turn it into a type. TypeScript can make that happen easily. Here’s an example:

const weekDays = ['Monday', 'TuesDay'] as const;

// type WeekDays = readonly ["Monday", "TuesDay"]
type WeekDays = typeof weekDays;

Explanation

• We define an array weekDays with two values: ‘Monday’ and ‘Tuesday’, and we use as const to make it a constant (so the values can’t be changed)
• By using typeof weekDays, we can extract the type from this array, which will be a readonly array containing “Monday” and “Tuesday”

Extracting Keys from an Array and Using Them in a Type

TypeScript can help you avoid mistakes when working with arrays and objects. If you add a new city, TypeScript will throw an error if you forget to add it as a key in citiesData. Let’s see how

const CITIES = ['Hyderabad', 'Bhainsa'] as const;

// CITIES_TYPE will be: "Hyderabad" | "Bhainsa"
export type CITIES_TYPE = (typeof CITIES)[number];

const citiesData: { [key in CITIES_TYPE]: string } = {
  Bhainsa: '',
  Hyderabad: ''
}

Explanation

• We define an array CITIES with city names and use as const to ensure they are treated as literal types
• CITIES_TYPE extracts each value from the array (“Hyderabad” and “Bhainsa”) and creates a union type “Hyderabad” | “Bhainsa”
• The citiesData object uses this type for its keys, meaning you must include all cities from the CITIES array. If you add a city to the array and forget to add it as a key in citiesData, TypeScript will catch it

Generic Functions with Argument Safety

Let’s say you want to write a generic function that finds duplicate values in an array of objects based on a key. TypeScript ensures you only pass valid keys from the objects, helping you avoid errors. Check out this example:

export const checkForDuplicatesByKey = <T, K extends keyof T>(
  items: T[],
  key: K
): boolean => {
  const seen = new Set<T[K]>();
  for (const item of items) {
    const value = item[key];
    if (seen.has(value)) {
      return true; // Duplicate found
    }
    seen.add(value);
  }
  return false; // No duplicates
}

type Item = {
  name: string;
}

type Items = Item[];

const items: Items = [
  { name: 'Hey' },
  { name: 'Hey' }
];

// This works fine because 'name' is a valid key in the objects
console.log(checkForDuplicatesByKey(items, 'name')); // true

// Error: 'ss' is not a valid key for the 'Item' type
checkForDuplicatesByKey(items, 'ss'); 
// Argument of type '"ss"' is not assignable to parameter of type '"name"'.ts(2345)

Explanation:

• The function checkForDuplicatesByKey is generic and works with any array of objects (T[]), but the key you pass must exist in the object
• TypeScript ensures that the key (K) is one of the properties in the object, avoiding mistakes where you pass an invalid key
• In this case, checkForDuplicatesByKey(items, ‘name’) works because ‘name’ is a valid key, but passing ‘ss’ will trigger a TypeScript error since it’s not part of the Item type

Conditional Argument Types

Sometimes, you want to change the input type a function accepts based on certain conditions. TypeScript makes this easy with conditional types, which adapt the input type depending on what you pass in. Let’s see how this works with an example using different currencies

export type USA = 'Dollar';
export type EUROPE = 'EURO';
export type INDIA = 'Rupee';

type USAConfig = {
  type: USA;
};

type EUROPEConfig = {
  type: EUROPE;
};

type INDIAConfig = {
  type: INDIA;
};

// CurrencyType can be USA, EUROPE, or INDIA
export type CurrencyType = USA | EUROPE | INDIA;

// Conditional logic to pick the correct config based on the currency type
export type ConfigType<T> = T extends USA
  ? USAConfig
  : T extends EUROPE
  ? EUROPEConfig
  : INDIAConfig;

export class ClassName {
  // The function adapts to the currency and expects the correct config
  static createInstance<T extends CurrencyType>(currency: T, config: ConfigType<T>) {
    // Function logic here
  }
}

// Valid cases: the config matches the currency type
ClassName.createInstance('Dollar', { type: 'Dollar' });
ClassName.createInstance('EURO', { type: 'EURO' });
ClassName.createInstance('Rupee', { type: 'Rupee' });

// Error: the config doesn't match the currency type
ClassName.createInstance('Rupee', { type: 'Dollar' }); // TypeScript catches this error

Explanation:

• We define three currencies: Dollar, EURO, and Rupee
• Each currency has its own configuration type (USAConfig, EUROPEConfig, INDIAConfig)
• The ConfigType uses conditional types to return the correct config type based on the currency
• The createInstance function adapts to the provided currency type and expects the correct configuration. If you try to mismatch, like passing a Dollar config when the currency is Rupee, TypeScript will give you an error


Sai Umesh

I’m Sai Umesh, a software engineer based in India. Working as a DevOps engineer.