johnieskills

Fixes TypeScript errors and strengthens your types.

Resolve TypeScript errors, eliminate any, and design complex types (generics, conditional, mapped, template literal, branded/opaque). Use for type-inference problems, infer / extends questions, utility types (Partial, Record, ReturnType, Awaited, NoInfer), satisfies, function overloads, declaration merging, and strict-mode refactors.

$ npx skills add johnie/skills --skill typescript-engineer
skills/typescript-engineer/references/utility-types.mdGitHub

TypeScript Utility Types

Contents

Overview

TypeScript provides built-in utility types that transform types in common ways. Mastering these utilities is essential for advanced TypeScript programming.

Parameters

Extracts the parameter types of a function type as a tuple:

function fetchUser(id: string, opts?: { timeout?: number }): Promise<User> {
  // ...
}

type FetchUserParams = Parameters<typeof fetchUser>;
// Type: [id: string, opts?: { timeout?: number } | undefined]

// Use in wrapper functions
const fetchUserWithLogging = async (
  ...args: Parameters<typeof fetchUser>
): Promise<User> => {
  console.log("Fetching user:", args[0]);
  return fetchUser(...args);
};

ReturnType

Extracts the return type of a function type:

function createUser(name: string, email: string) {
  return {
    id: crypto.randomUUID(),
    name,
    email,
    createdAt: new Date(),
  };
}

type User = ReturnType<typeof createUser>;
// Type: { id: string; name: string; email: string; createdAt: Date }

Awaited

Unwraps the type inside a Promise (including nested Promises):

type PromiseString = Promise<string>;
type NestedPromise = Promise<Promise<number>>;

type Unwrapped1 = Awaited<PromiseString>; // string
type Unwrapped2 = Awaited<NestedPromise>; // number

// Combine with ReturnType for async functions
async function fetchUser(id: string): Promise<User> {
  // ...
}

type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>;
// Type: User (not Promise<User>)

Pattern: Wrapping External Library Functions

When extending functions from external libraries that don't export their types:

import { fetchUser } from "external-lib";

// Extract and extend the return type
type FetchUserReturn = Awaited<ReturnType<typeof fetchUser>>;

export const fetchUserWithFullName = async (
  ...args: Parameters<typeof fetchUser>
): Promise<FetchUserReturn & { fullName: string }> => {
  const user = await fetchUser(...args);
  return {
    ...user,
    fullName: `${user.firstName} ${user.lastName}`,
  };
};

Record<Keys, Type>

Creates an object type with specified keys and value type:

type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;

const rolePermissions: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Dynamic keys with constraint
function createLookup<K extends string, V>(
  keys: K[],
  getValue: (key: K) => V
): Record<K, V> {
  const result = {} as Record<K, V>;
  for (const key of keys) {
    result[key] = getValue(key);
  }
  return result;
}

Partial

Makes all properties optional:

interface User {
  id: string;
  name: string;
  email: string;
}

type UpdateUserInput = Partial<User>;
// Type: { id?: string; name?: string; email?: string }

function updateUser(id: string, updates: Partial<User>): User {
  // ...
}

updateUser("123", { name: "New Name" }); // OK - only updating name

Required

Makes all properties required (opposite of Partial):

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

type RequiredConfig = Required<Config>;
// Type: { host: string; port: number; debug: boolean }

Omit<T, Keys>

Creates a type by omitting specified properties:

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

type PublicUser = Omit<User, "password">;
// Type: { id: string; name: string; email: string }

type CreateUserInput = Omit<User, "id">;
// Type: { name: string; email: string; password: string }

Pick<T, Keys>

Creates a type by picking specified properties (opposite of Omit):

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type UserCredentials = Pick<User, "email" | "password">;
// Type: { email: string; password: string }

Exclude<T, U> and Extract<T, U>

Work with union types:

type AllColors = "red" | "green" | "blue" | "yellow";

type PrimaryColors = Extract<AllColors, "red" | "blue">;
// Type: "red" | "blue"

type NonPrimaryColors = Exclude<AllColors, "red" | "blue">;
// Type: "green" | "yellow"

NonNullable

Removes null and undefined from a type:

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Type: string

Creating a Reusable Wrapper Type

Combine utilities to create reusable type helpers:

// A type that wraps any async function, extending its return type
type WrapFunction<TFunc extends (...args: any) => any, TAdditional = {}> = (
  ...args: Parameters<TFunc>
) => Promise<Awaited<ReturnType<TFunc>> & TAdditional>;

// Usage
import { fetchUser, fetchPost } from "external-lib";

const fetchUserWithMeta: WrapFunction<
  typeof fetchUser,
  { meta: { fetchedAt: Date } }
> = async (...args) => {
  const user = await fetchUser(...args);
  return {
    ...user,
    meta: { fetchedAt: new Date() },
  };
};

When to Use Each Utility

Utility Use Case
Parameters<T> Wrapping functions, creating function variants
ReturnType<T> Extracting return types when not explicitly exported
Awaited<T> Unwrapping Promise types
Record<K, V> Creating object types with dynamic keys
Partial<T> Update/patch operations
Required<T> Ensuring all config options are provided
Omit<T, K> Removing sensitive or internal fields
Pick<T, K> Creating focused subsets of types
Exclude<T, U> Filtering union types
Extract<T, U> Selecting from union types
NonNullable<T> Removing null/undefined after validation

NoInfer<T> (TS 5.4+)

Prevents a type parameter position from contributing to inference. Useful when a generic has multiple use sites and you want one site to drive inference while the other just consumes it.

// Without NoInfer — second argument can widen the inferred type
function createFSM<TState extends string>(initial: TState, states: TState[]) {}

// "off" | "on" | "broken" — "broken" sneaks in via states
createFSM("off", ["on", "off", "broken"]);

// With NoInfer — only initial drives inference
function createFSM<TState extends string>(
  initial: TState,
  states: NoInfer<TState>[]
) {}

createFSM("off", ["on", "off", "broken"]);
//                              ^^^^^^^^ Error: "broken" is not assignable to "off" | "on"

Typical use cases

Default values that shouldn't widen the type:

function getOrDefault<T>(value: T | undefined, defaultValue: NoInfer<T>): T {
  return value ?? defaultValue;
}

// T is inferred as "a" | "b" from the first argument only
getOrDefault<"a" | "b">("a", "c"); // Error: "c" is not "a" | "b"

Event emitters where the event name drives the payload type:

function on<TEvent extends keyof EventMap>(
  event: TEvent,
  handler: (payload: NoInfer<EventMap[TEvent]>) => void
) {}

Common Pitfalls

Using ReturnType on Async Functions

async function getData(): Promise<string[]> {
  return ["data"];
}

// This gives Promise<string[]>, not string[]
type Wrong = ReturnType<typeof getData>; // Promise<string[]>

// Use Awaited to unwrap
type Right = Awaited<ReturnType<typeof getData>>; // string[]

Forgetting typeof for Runtime Functions

function myFunc(x: number): string {
  return String(x);
}

// Wrong - myFunc is a value, not a type
type Params = Parameters<myFunc>; // Error

// Correct - use typeof
type Params = Parameters<typeof myFunc>; // [x: number]