Typescript Typescript

Transforming Types with TypeScript's Built-In Utility Types

Dima Июн 24, 2026

Introduction

A production codebase rarely works with a single version of a data type. An admin dashboard needs to display a user record as read-only, accept a partially filled draft form, accept only specific fields for a patch update, and validate the full shape before saving. Each of these is a different type — but they are all derived from the same underlying entity. The naive response is to write four separate interfaces, each a copy of the original with fields added, removed, or modified. That copy-paste strategy works until the entity grows by two fields: now four interfaces must be updated, and the ones that were forgotten silently diverge.

TypeScript ships with a set of built-in utility types — Partial, Required, Readonly, Pick, Omit, Record, ReturnType, Parameters, NonNullable — that derive new types from existing ones without duplicating field lists. Instead of four hand-written interfaces, you write one authoritative entity type and derive the rest algebraically. When the entity gains a field, every derived type gains it automatically.

This tutorial builds a form management library for an admin dashboard. The library manages user account records through their full lifecycle: draft creation, patch updates, display views, and validation. Every utility type is introduced because the scenario genuinely requires it — not as a demo, but as the natural solution to a specific type-safety problem the library faces.


Background

TypeScript's utility types are generic types built into the language's standard library. They are syntactic sugar over mapped types — types that iterate over the keys of an existing type and transform each property. The key mechanisms behind them are:

  • keyof T — produces a union of the string keys of type T; for { id: string; name: string } it produces "id" | "name"
  • Mapped types — iterate over a set of keys and produce a new object type: { [K in keyof T]: ... }
  • Conditional types — select between types based on a condition: T extends U ? A : B
  • The as clause — renames keys inside a mapped type: { [K in keyof T as NewKey]: ... }

The built-in utility types use these primitives:

  • Partial<T> — makes every property of T optional
  • Required<T> — makes every property of T required, removing ?
  • Readonly<T> — makes every property of T immutable
  • Pick<T, K> — produces a type with only the keys listed in K
  • Omit<T, K> — produces a type with every key of T except those in K
  • Record<K, V> — produces an object type with keys K and values V
  • ReturnType<F> — extracts the return type of a function type F
  • Parameters<F> — extracts the parameter types of a function type F as a tuple
  • NonNullable<T> — removes null and undefined from a union type

Practical Scenario

The admin dashboard for a SaaS platform manages user accounts. Each account has a fixed set of fields: a unique identifier, display name, email, role, subscription tier, and a set of optional preferences. The engineering team needs four different type-safe operations on these account records.

First, the account creation flow uses a multi-step form that saves a draft after each step. At draft time, most fields are not yet filled in — the type must allow any subset of fields to be present. Second, the patch update API accepts partial changes and merges them into the existing record: it must accept only specific fields (not the immutable id or system-managed createdAt) and all of them are optional. Third, the account display component receives a read-only projection of the account for rendering — it must not be able to mutate the record, and it only needs a subset of fields for display. Fourth, the validation layer must ensure a complete, fully-populated record before saving to the database.

Managing these four variants manually means four interface definitions with overlapping field lists. The team has already discovered the problem: when a new organizationId field was added to the account schema, the draft type was updated but the patch type was not, and the API silently ignored organizationId in patch requests for three weeks.


The Problem

The naive approach copies interface fields into four separate types by hand.

Create a new file:

touch admin-forms.ts

Run it using:

npx tsx admin-forms.ts
// The single source of truth — but it has to be maintained manually in sync
// with DraftUserAccount, PatchUserAccount, DisplayUserAccount, and ValidUserAccount below.

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

// Duplicates every field as optional
interface DraftUserAccount {
  id?: string;
  displayName?: string;
  email?: string;
  role?: "admin" | "editor" | "viewer";
  subscriptionTier?: "free" | "pro" | "enterprise";
  createdAt?: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

// Duplicates a subset — will silently drift when UserAccount changes
interface PatchUserAccount {
  displayName?: string;
  email?: string;
  role?: "admin" | "editor" | "viewer";
  subscriptionTier?: "free" | "pro" | "enterprise";
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

// Another subset for display — fields copied again
interface DisplayUserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
}

function saveDraft(draft: DraftUserAccount): void {
  console.log(`Saving draft: ${JSON.stringify(draft)}`);
}

function applyPatch(id: string, patch: PatchUserAccount): void {
  console.log(`Patching ${id}: ${JSON.stringify(patch)}`);
}

function renderAccountCard(account: DisplayUserAccount): void {
  console.log(`[${account.role.toUpperCase()}] ${account.displayName} <${account.email}>`);
}

saveDraft({ displayName: "Alice Chen", email: "alice@example.com" });
applyPatch("usr-001", { role: "editor" });
renderAccountCard({ id: "usr-001", displayName: "Alice Chen", email: "alice@example.com", role: "editor" });


Saving draft: {"displayName":"Alice Chen","email":"alice@example.com"}
Patching usr-001: {"role":"editor"}
[EDITOR] Alice Chen <alice@example.com>


The code runs, but the type definitions are a maintenance hazard. DraftUserAccount and PatchUserAccount duplicate the preferences field literal entirely — when the preferences shape changes, both must be updated. DisplayUserAccount copies four fields from UserAccount by hand. Adding organizationId to UserAccount requires visiting every derived interface and deciding whether it belongs there too. The compiler cannot help with this because the types are independent declarations — a field added to UserAccount has no effect on the others.


Partial<T> for Draft States

Partial<T> makes every property of a type optional. For a draft form that is filled in step by step, this is exactly the right semantics: any subset of fields may be present, and the type should reflect that without duplicating the field list.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

type DraftUserAccount = Partial<UserAccount>;

function saveDraft(draft: DraftUserAccount): void {
  const fields = Object.keys(draft).join(", ") || "(empty)";
  console.log(`Saving draft with fields: ${fields}`);
}

const emptyDraft: DraftUserAccount = {};
const partialDraft: DraftUserAccount = {
  displayName: "Alice Chen",
  email: "alice@example.com",
};

saveDraft(emptyDraft);
saveDraft(partialDraft);


Saving draft with fields: (empty)
Saving draft with fields: displayName, email


DraftUserAccount is now a single line that tracks UserAccount exactly. If organizationId is added to UserAccount, DraftUserAccount automatically becomes { organizationId?: string; ... } — no manual update required.

Partial<T> eliminates the synchronization problem entirely. There is no second interface to update, no field list to keep in sync, and no risk of forgetting a field. The derived type is defined as a transformation of the source, so changes to the source propagate automatically. Any code that was relying on the drift — intentional or not — becomes a compile error immediately.


Required<T> for Validated Records

Before saving to the database, every field must be present. Required<T> is the inverse of Partial<T> — it removes the optional modifier from every property. A function that accepts only fully-populated accounts can use Required<T> to express that guarantee precisely.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

type DraftUserAccount = Partial<UserAccount>;
type ValidatedUserAccount = Required<UserAccount>;

function validateAndSave(account: ValidatedUserAccount): void {
  console.log(`Saving validated account: ${account.id}${account.displayName}`);
  console.log(`  Role: ${account.role}, Tier: ${account.subscriptionTier}`);
  console.log(`  Theme: ${account.preferences.theme}`);
}

const complete: ValidatedUserAccount = {
  id: "usr-001",
  displayName: "Alice Chen",
  email: "alice@example.com",
  role: "editor",
  subscriptionTier: "pro",
  createdAt: "2024-06-01T09:00:00Z",
  preferences: { theme: "dark", emailNotifications: true },
};

validateAndSave(complete);

// Uncomment to see compiler error — preferences is required here:
// validateAndSave({ id: "usr-002", displayName: "Bob", email: "bob@example.com",
//   role: "viewer", subscriptionTier: "free", createdAt: "2024-06-01T09:00:00Z" });


Saving validated account: usr-001  Alice Chen
  Role: editor, Tier: pro
  Theme: dark


ValidatedUserAccount requires preferences even though UserAccount marks it optional. Inside validateAndSave, accessing account.preferences.theme is safe — the compiler knows preferences cannot be undefined on a ValidatedUserAccount.

Required<T> expresses a post-validation invariant at the type level: a function that receives a ValidatedUserAccount has a compiler guarantee that no field is absent. The alternative — writing a separate FullUserAccount interface with every field required — would again duplicate the field list and could silently omit preferences from the required set if a developer forgot to remove the ?.


Readonly<T> for Display Views

The display layer should receive account data and render it — it should never mutate it. Readonly<T> makes every property immutable at the type level, turning an accidental assignment into a compile error rather than a subtle state corruption bug.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

type ReadonlyUserAccount = Readonly<UserAccount>;

function renderAccountCard(account: ReadonlyUserAccount): void {
  const tier = account.subscriptionTier.toUpperCase();
  const role = account.role.toUpperCase();
  console.log(`[${tier}] ${account.displayName}${role}`);
  console.log(`  Email: ${account.email}`);
  console.log(`  Member since: ${account.createdAt.slice(0, 10)}`);

  // Uncomment to see compiler error:
  // account.displayName = "Hacked";
}

const account: ReadonlyUserAccount = {
  id: "usr-001",
  displayName: "Alice Chen",
  email: "alice@example.com",
  role: "editor",
  subscriptionTier: "pro",
  createdAt: "2024-06-01T09:00:00Z",
};

renderAccountCard(account);


[PRO] Alice Chen  EDITOR
  Email: alice@example.com
  Member since: 2024-06-01


The commented-out assignment would produce: Cannot assign to 'displayName' because it is a read-only property. The renderer receives the account, uses it, and cannot accidentally mutate state that might be shared with other components.

Readonly<T> makes the immutability intent machine-checkable. In a component tree where a parent passes account data down to multiple child components, a mutable type means any child could silently alter the shared object. Readonly<T> makes accidental mutation a compile error — and unlike Object.freeze, it has zero runtime cost.

Note: Readonly<T> is shallow — it freezes the top-level properties but not nested objects. account.preferences.theme could still be reassigned. For deep immutability, a recursive utility type is needed.


Pick<T, K> and Omit<T, K> for Targeted Projections

Pick<T, K> extracts a named subset of fields. Omit<T, K> removes named fields and keeps the rest. Both are more precise than Partial<T> when the goal is a specific subset, not a fully optional version of the whole type.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

// Display card only needs these four fields
type AccountSummary = Pick<UserAccount, "id" | "displayName" | "email" | "role">;

// Patch updates cannot change id or createdAt — omit them, then make the rest optional
type PatchUserAccount = Partial<Omit<UserAccount, "id" | "createdAt">>;

function renderSummary(account: AccountSummary): void {
  console.log(`[${account.role}] ${account.displayName}${account.email}`);
}

function applyPatch(id: string, patch: PatchUserAccount): void {
  const keys = Object.keys(patch).join(", ") || "(no changes)";
  console.log(`Patch for ${id}: ${keys}`);
}

const summary: AccountSummary = {
  id: "usr-001",
  displayName: "Alice Chen",
  email: "alice@example.com",
  role: "editor",
};

renderSummary(summary);
applyPatch("usr-001", { role: "admin", subscriptionTier: "enterprise" });
applyPatch("usr-002", { displayName: "Bob Zhang" });
applyPatch("usr-003", {});


[editor] Alice Chen  alice@example.com
Patch for usr-001: role, subscriptionTier
Patch for usr-002: displayName
Patch for usr-003: (no changes)


PatchUserAccount is Partial<Omit<UserAccount, "id" | "createdAt">> — a single composed expression that means "every field from UserAccount except the immutable ones, all made optional." Any attempt to include id or createdAt in a patch object is a compile error.

Pick and Omit let you express the intent ("only these fields" or "everything except these fields") rather than enumerating the fields by hand. When UserAccount gains a new mutable field like organizationId, PatchUserAccount automatically includes it as an optional patch target — without any manual update. A hand-written interface would silently ignore the new field until someone noticed it was missing from the patch API.


Record<K, V> for Structured Lookup Maps

Record<K, V> creates an object type where every key is drawn from K and every value is V. It is the right tool for lookup tables, indexed caches, and validation error maps — any structure where the key set is known and the value type is uniform.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

type AccountField = keyof UserAccount;
type ValidationErrors = Record<AccountField, string | null>;
type RolePermissions = Record<UserAccount["role"], string[]>;

const errors: ValidationErrors = {
  id: null,
  displayName: "Display name must be at least 2 characters",
  email: "Invalid email format",
  role: null,
  subscriptionTier: null,
  createdAt: null,
  preferences: null,
};

const permissions: RolePermissions = {
  admin: ["read", "write", "delete", "manage-users"],
  editor: ["read", "write"],
  viewer: ["read"],
};

function printValidationReport(errs: ValidationErrors): void {
  const failed = (Object.entries(errs) as [AccountField, string | null][])
    .filter(([, msg]) => msg !== null);
  if (failed.length === 0) {
    console.log("Validation passed");
    return;
  }
  console.log(`Validation failed (${failed.length} error(s)):`);
  for (const [field, msg] of failed) {
    console.log(`  ${field}: ${msg}`);
  }
}

function printPermissions(role: UserAccount["role"]): void {
  console.log(`${role}: ${permissions[role].join(", ")}`);
}

printValidationReport(errors);
printPermissions("admin");
printPermissions("viewer");


Validation failed (2 error(s)):
  displayName: Display name must be at least 2 characters
  email: Invalid email format
admin: read, write, delete, manage-users
viewer: read


Record<AccountField, string | null> requires a key for every field in UserAccount. If organizationId is added to the interface, any ValidationErrors object that is missing the organizationId key becomes a compile error — the validation map cannot silently skip the new field.

Record<K, V> turns a plain object into a type-safe map where exhaustiveness is enforced by the compiler. An errors object that is missing email is a compile error, not a runtime gap. Record<UserAccount["role"], string[]> uses a literal union as the key type, so any attempt to add a role that does not exist in UserAccount is rejected, and every valid role is guaranteed to be present.


ReturnType<F> and Parameters<F>

ReturnType<F> extracts the type a function returns. Parameters<F> extracts the parameter types as a tuple. Both are essential when working with functions whose signatures you do not control — middleware, decorators, or factory functions — where repeating the type manually would create a maintenance dependency.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
}

function buildAccountRecord(
  id: string,
  displayName: string,
  email: string,
  role: UserAccount["role"],
  subscriptionTier: UserAccount["subscriptionTier"]
): UserAccount {
  return {
    id,
    displayName,
    email,
    role,
    subscriptionTier,
    createdAt: new Date().toISOString(),
  };
}

// Derived from the function — no manual repetition
type AccountRecord = ReturnType<typeof buildAccountRecord>;
type BuildArgs = Parameters<typeof buildAccountRecord>;

function auditAccountCreation(args: BuildArgs): void {
  const [id, displayName, , role] = args;
  console.log(`Audit: creating account ${id} for ${displayName} with role ${role}`);
}

function createAndLog(...args: BuildArgs): AccountRecord {
  auditAccountCreation(args);
  const account = buildAccountRecord(...args);
  console.log(`Created: ${account.id} at ${account.createdAt.slice(0, 10)}`);
  return account;
}

createAndLog("usr-001", "Alice Chen", "alice@example.com", "editor", "pro");
createAndLog("usr-002", "Bob Zhang", "bob@example.com", "viewer", "free");


Audit: creating account usr-001 for Alice Chen with role editor
Created: usr-001 at 2024-06-01
Audit: creating account usr-002 for Bob Zhang with role viewer
Created: usr-002 at 2024-06-01


ReturnType<typeof buildAccountRecord> is UserAccount — extracted from the function, not written by hand. Parameters<typeof buildAccountRecord> is the tuple [string, string, string, "admin" | "editor" | "viewer", "free" | "pro" | "enterprise"]. If the function signature changes, every type derived from it updates automatically.

ReturnType and Parameters are critical when consuming functions you do not own — a third-party SDK, a generated API client, a shared utility. Instead of manually copying the return type (which drifts when the library updates), you derive it. createAndLog is guaranteed to accept exactly the same arguments as buildAccountRecord, enforced by the compiler, without any duplicated type annotation.


NonNullable<T> for Eliminating Undefined

NonNullable<T> removes null and undefined from a union type. It narrows the type of a value after a null check has been performed — useful when a field is optional in storage but required at the point of use.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
  preferences?: {
    theme: "light" | "dark";
    emailNotifications: boolean;
  };
}

type AccountPreferences = NonNullable<UserAccount["preferences"]>;

function applyTheme(prefs: AccountPreferences): void {
  console.log(`Applying theme: ${prefs.theme}`);
  console.log(`Email notifications: ${prefs.emailNotifications ? "on" : "off"}`);
}

function initializeSession(account: UserAccount): void {
  const prefs: AccountPreferences = account.preferences ?? {
    theme: "light",
    emailNotifications: true,
  };
  console.log(`Session for ${account.displayName}:`);
  applyTheme(prefs);
}

const accountWithPrefs: UserAccount = {
  id: "usr-001",
  displayName: "Alice Chen",
  email: "alice@example.com",
  role: "editor",
  subscriptionTier: "pro",
  createdAt: "2024-06-01T09:00:00Z",
  preferences: { theme: "dark", emailNotifications: false },
};

const accountWithoutPrefs: UserAccount = {
  id: "usr-002",
  displayName: "Bob Zhang",
  email: "bob@example.com",
  role: "viewer",
  subscriptionTier: "free",
  createdAt: "2024-06-01T09:00:00Z",
};

initializeSession(accountWithPrefs);
initializeSession(accountWithoutPrefs);


Session for Alice Chen:
Applying theme: dark
Email notifications: off
Session for Bob Zhang:
Applying theme: light
Email notifications: on


AccountPreferences is { theme: "light" | "dark"; emailNotifications: boolean } — the undefined removed. applyTheme accepts AccountPreferences, not UserAccount["preferences"], so the compiler knows the caller has already resolved the optional. Passing account.preferences directly to applyTheme would be a compile error without the null-coalescing fallback.

NonNullable<T> names the type of a value after a guard has been applied, making the narrowing explicit in the function signature rather than hidden inside the function body. A function typed to accept AccountPreferences declares at its boundary that undefined has been handled by the caller — which is a meaningful contract, not just a style choice.


Custom Mapped Types with keyof and the as Clause

The built-in utility types cover the common cases, but the same machinery is available directly. A mapped type iterates over keyof T, and the as clause renames each key during iteration — enabling a prefix convention, a camelCase-to-snake_case transform, or any systematic key rename.

Replace the entire content of admin-forms.ts with the following:

interface UserAccount {
  id: string;
  displayName: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  subscriptionTier: "free" | "pro" | "enterprise";
  createdAt: string;
}

// Prefix every key with "form_" to match HTML form field naming conventions
type FormFields<T> = {
  [K in keyof T as `form_${string & K}`]: T[K];
};

// Make every value a validator function signature
type Validators<T> = {
  [K in keyof T]: (value: T[K]) => string | null;
};

type AccountFormFields = FormFields<UserAccount>;
type AccountValidators = Validators<UserAccount>;

const formData: AccountFormFields = {
  form_id: "usr-001",
  form_displayName: "Alice Chen",
  form_email: "alice@example.com",
  form_role: "editor",
  form_subscriptionTier: "pro",
  form_createdAt: "2024-06-01T09:00:00Z",
};

const validators: AccountValidators = {
  id: (v) => (v.length > 0 ? null : "ID cannot be empty"),
  displayName: (v) => (v.length >= 2 ? null : "Must be at least 2 characters"),
  email: (v) => (v.includes("@") ? null : "Invalid email format"),
  role: (_v) => null,
  subscriptionTier: (_v) => null,
  createdAt: (v) => (v.length > 0 ? null : "Created date required"),
};

function validateAccount(account: UserAccount): void {
  let valid = true;
  for (const key of Object.keys(validators) as (keyof UserAccount)[]) {
    const error = validators[key](account[key] as never);
    if (error !== null) {
      console.log(`  ${key}: ${error}`);
      valid = false;
    }
  }
  if (valid) console.log("All fields valid");
}

console.log(`Form field: ${formData.form_displayName}`);
console.log("Validating complete account:");
validateAccount({
  id: "usr-001",
  displayName: "Alice Chen",
  email: "alice@example.com",
  role: "editor",
  subscriptionTier: "pro",
  createdAt: "2024-06-01T09:00:00Z",
});
console.log("Validating incomplete account:");
validateAccount({
  id: "",
  displayName: "A",
  email: "not-an-email",
  role: "viewer",
  subscriptionTier: "free",
  createdAt: "",
});


Form field: Alice Chen
Validating complete account:
All fields valid
Validating incomplete account:
  id: ID cannot be empty
  displayName: Must be at least 2 characters
  email: Invalid email format
  createdAt: Created date required


FormFields<UserAccount> produces a type with keys form_id, form_displayName, form_email, etc. The as clause rewrites each key using a template literal. Validators<UserAccount> produces a type where every key maps to a function that accepts the corresponding field's type — validators.email must accept a string, not a number.

Custom mapped types let you express systematic transformations of a type that no built-in utility covers. The Validators<T> pattern guarantees that the validator object has exactly one entry per field — no more, no fewer — and that each validator's parameter type matches the field type. Adding a field to UserAccount immediately requires a matching validator, enforced at compile time.


Summary

TypeScript's built-in utility types replace hand-copied interface duplicates with algebraic transformations of a single source-of-truth type. The admin dashboard's form management library derived six distinct type variants from one UserAccount interface, with the guarantee that adding or renaming a field on the source propagates to every derived type automatically:

  • Partial<T> makes every property optional and is the right type for draft or in-progress form states where any subset of fields may be present
  • Required<T> removes optional modifiers and is the right type for values that have passed validation and are guaranteed to be fully populated
  • Readonly<T> makes every property immutable at the type level, preventing accidental mutation in display or pure-read components; it is shallow and does not protect nested objects
  • Pick<T, K> extracts a named subset of fields and is the right type for projections that only need specific fields from a larger entity
  • Omit<T, K> removes named fields and keeps the rest; composing Partial<Omit<T, K>> is the standard pattern for patch-update types that exclude immutable fields
  • Record<K, V> produces an indexed object type with a known key set and uniform value type; when K is derived from keyof T or a literal union, the compiler enforces exhaustiveness
  • ReturnType<F> and Parameters<F> extract types from function signatures, eliminating the need to repeat type annotations when consuming functions whose signatures you do not control
  • NonNullable<T> strips null and undefined from a union, giving a narrowed type a name that reflects the guarantee the caller has already provided
  • Custom mapped types with keyof T and the as clause express systematic key transformations — renaming, prefixing, filtering — that no built-in utility covers, while preserving exhaustiveness enforcement

Чтобы получить доступ к облачной лаборатории, необходимо войти в систему.

Войти