# type-guards

This library provides a set of type guards that help safely check and narrow down types at runtime, making it easier to handle data validation and type checking in a type-safe manner. It also ships a set of general-purpose utility functions and utility types.

## Install

```sh
npm install @wistia/type-guards
```

```sh
yarn add @wistia/type-guards
```

```sh
pnpm add @wistia/type-guards
```

## Usage

Every guard is a `value is T` predicate, so TypeScript narrows the value inside the `if` block:

```ts
import { isNonEmptyString, isNotNil } from '@wistia/type-guards';

function greet(value: unknown): string {
  if (isNonEmptyString(value)) {
    return `Hello, ${value}`;
  }
  return 'Hello, stranger';
}

const names: (string | null | undefined)[] = ['Alice', null, 'Bob'];
const cleaned: string[] = names.filter(isNotNil); // ['Alice', 'Bob']
```

## Guards

<!-- AUTOGEN:README-GUARDS:START -->

### Primitives

| Guard                    | Narrows to                                 |
| ------------------------ | ------------------------------------------ |
| `isAsyncFunction(value)` | `(...args: unknown[]) => Promise<unknown>` |
| `isBigInt(value)`        | `bigint`                                   |
| `isBoolean(value)`       | `boolean`                                  |
| `isDate(value)`          | `Date`                                     |
| `isError(value)`         | `Error`                                    |
| `isFiniteNumber(value)`  | `number`                                   |
| `isFunction(value)`      | `(...args: unknown[]) => unknown`          |
| `isInteger(value)`       | `number`                                   |
| `isIterable(value)`      | `Iterable<unknown>`                        |
| `isMap(value)`           | `Map<unknown, unknown>`                    |
| `isNaN(value)`           | `number`                                   |
| `isNil(value)`           | `Nil`                                      |
| `isNonNaNNumber(value)`  | `number`                                   |
| `isNull(value)`          | `null`                                     |
| `isNumber(value)`        | `number`                                   |
| `isPromise(value)`       | `Promise<unknown>`                         |
| `isRegExp(value)`        | `RegExp`                                   |
| `isSet(value)`           | `Set<unknown>`                             |
| `isString(value)`        | `string`                                   |
| `isSymbol(value)`        | `symbol`                                   |
| `isUndefined(value)`     | `undefined`                                |
| `isVoid(value)`          | `void`                                     |
| `isWeakMap(value)`       | `WeakMap<WeakKey, unknown>`                |
| `isWeakSet(value)`       | `WeakSet<WeakKey>`                         |

### Arrays

| Guard                              | Narrows to         |
| ---------------------------------- | ------------------ |
| `isArray(value)`                   | `unknown[]`        |
| `isEmptyArray(value)`              | `never[]`          |
| `isNonEmptyArray(value)`           | `NonEmptyArray<T>` |
| `readonlyArrayIncludes(arr, elem)` | `T`                |

### Records (plain objects, not arrays)

| Guard                     | Narrows to                |
| ------------------------- | ------------------------- |
| `hasKey(value, key)`      | `Record<Key, Value>`      |
| `isEmptyRecord(value)`    | `EmptyObject`             |
| `isNonEmptyRecord(value)` | `Record<string, unknown>` |
| `isPlainObject(value)`    | `Record<string, unknown>` |
| `isRecord(value)`         | `Record<string, unknown>` |

### Strings

| Guard                     | Narrows to |
| ------------------------- | ---------- |
| `isEmptyString(value)`    | `''`       |
| `isNonBlankString(value)` | `string`   |
| `isNonEmptyString(value)` | `string`   |

### Truthy / falsy

| Guard             | Narrows to |
| ----------------- | ---------- |
| `isFalsy(value)`  | `Falsy`    |
| `isTruthy(value)` | `boolean`  |

### Negated

The `isNot*` variants are useful as `.filter` predicates to drop the unwanted type:

| Guard                   | Narrows to                                          |
| ----------------------- | --------------------------------------------------- |
| `isNotArray(value)`     | `Exclude<unknown, unknown[]>`                       |
| `isNotBoolean(value)`   | `Exclude<unknown, boolean>`                         |
| `isNotFunction(value)`  | `Exclude<unknown, (...args: unknown[]) => unknown>` |
| `isNotNil(value)`       | `T`                                                 |
| `isNotNull(value)`      | `Exclude<T, null>`                                  |
| `isNotNumber(value)`    | `Exclude<unknown, number>`                          |
| `isNotRecord(value)`    | `Exclude<unknown, Record<string, unknown>>`         |
| `isNotString(value)`    | `Exclude<unknown, string>`                          |
| `isNotUndefined(value)` | `Exclude<T, undefined>`                             |
| `isNotVoid(value)`      | `Exclude<unknown, void>`                            |

### DOM

| Guard                        | Narrows to          |
| ---------------------------- | ------------------- |
| `isHtmlButtonElement(value)` | `HTMLButtonElement` |
| `isHtmlElement(value)`       | `HTMLElement`       |
| `isHtmlInputElement(value)`  | `HTMLInputElement`  |
| `isHtmlVideoElement(value)`  | `HTMLVideoElement`  |
| `isMouseEvent(value)`        | `MouseEvent`        |
| `isSvgElement(value)`        | `SVGElement`        |
| `isTextNode(value)`          | `Text`              |

<!-- AUTOGEN:README-GUARDS:END -->

## Types

<!-- AUTOGEN:README-TYPES:START -->

| Type                                                | Resolves to                                                   |
| --------------------------------------------------- | ------------------------------------------------------------- |
| `Arrayable<T>`                                      | `T \| T[]`                                                    |
| `Falsy`                                             | `typeof Number.NaN \| '' \| 0n \| false \| null \| undefined` |
| `NestedNonNullable<T, P extends Paths<T> & string>` | `NonNullable<Get<T, P>>`                                      |
| `Nil`                                               | `null \| undefined`                                           |
| `Nilable<A>`                                        | `A \| Nil`                                                    |
| `NilableArray<T>`                                   | `Nilable<Nilable<T>[]>`                                       |
| `NonEmptyArray<T>`                                  | `[T, ...T[]]`                                                 |
| `NotNilable<A>`                                     | `Exclude<A, Nil>`                                             |
| `Nullable<A>`                                       | `A \| null`                                                   |
| `NullableProperties<T extends UnknownRecord>`       | `{ [Key in keyof T]: Nullable<T[Key]> }`                      |
| `Truthy<T>`                                         | `Exclude<T, Falsy>`                                           |
| `Undefinable<A>`                                    | `A \| undefined`                                              |

<!-- AUTOGEN:README-TYPES:END -->

## Utilities

General-purpose helper functions that complement the guards. Unlike guards, these are not type predicates.

<!-- AUTOGEN:README-UTILITIES:START -->

| Utility                                                                  | Returns                        |
| ------------------------------------------------------------------------ | ------------------------------ |
| `buildTimeDuration(numberOfMilliseconds)`                                | `TimeDuration`                 |
| `coerceToBoolean(value)`                                                 | `boolean`                      |
| `dateOnlyISOString(date, { timeZone = defaultTimeZone })`                | `string`                       |
| `dateOnlyString(date, { timeZone = defaultTimeZone, omitYear = false })` | `string`                       |
| `dateOnlyStringForSentence(date, { timeZone = defaultTimeZone })`        | `string`                       |
| `dateOnlyStringNumeric(date, { timeZone = defaultTimeZone })`            | `string`                       |
| `dateTimeRounded(dateTime, toISOString = true)`                          | `string \| Date \| null`       |
| `dateTimeString(date, { timeZone = defaultTimeZone, omitYear = false })` | `string`                       |
| `dateTimeStringForSentence(date, { timeZone = defaultTimeZone })`        | `string`                       |
| `dateTimeToDate(dateTime)`                                               | `Date \| null`                 |
| `dateTimeToISO(dateTime)`                                                | `string \| null`               |
| `dateToDateTime(date)`                                                   | `WistiaDateTimeObject \| null` |
| `dateUTCOffset(date)`                                                    | `string`                       |
| `dayOfWeekString(date, { timeZone = defaultTimeZone })`                  | `string`                       |
| `deepMerge(...objects)`                                                  | `DeepMergeResult<T>`           |
| `getObjectEntries(obj)`                                                  | `[keyof T, T[keyof T]][]`      |
| `getObjectKeys(obj)`                                                     | `(keyof T)[]`                  |
| `getObjectValues(obj)`                                                   | `T[keyof T][]`                 |
| `isUrl(value)`                                                           | `boolean`                      |
| `mediaDurationString(numberOfMilliseconds)`                              | `string`                       |
| `millisecondsToDurationISOString(numberOfMilliseconds)`                  | `string`                       |
| `monthDayStringNumeric(date, { timeZone = defaultTimeZone })`            | `string`                       |
| `parseDateString(str)`                                                   | `Date \| null`                 |
| `sessionDurationString(numberOfMilliseconds)`                            | `string`                       |
| `stripExtension(str)`                                                    | `string`                       |
| `timeAgoString(date, { nowAnchor = new Date(), includeTime = true })`    | `string`                       |
| `timeOnlyString(date, { timeZone = defaultTimeZone })`                   | `string`                       |
| `tupleIncludes(tuple, item)`                                             | `item is TTuple[number]`       |

<!-- AUTOGEN:README-UTILITIES:END -->

## Patterns

Filter nullish values from a list, with narrowing:

```ts
import { isNotNil } from '@wistia/type-guards';

const raw: (string | null | undefined)[] = ['a', null, 'b', undefined];
const clean: string[] = raw.filter(isNotNil); // ['a', 'b']
```

Safely access a key on an unknown value:

```ts
import { hasKey } from '@wistia/type-guards';

function readName(value: unknown): string | undefined {
  if (hasKey<'name', string>(value, 'name') && typeof value.name === 'string') {
    return value.name;
  }
  return undefined;
}
```

Combine guards to handle a discriminated input:

```ts
import { isString, isNonEmptyArray } from '@wistia/type-guards';

function describe(value: unknown): string {
  if (isString(value)) return value.toUpperCase();
  if (isNonEmptyArray<unknown>(value)) return `${value.length} items`;
  return 'other';
}
```

## Agent-friendly docs

Every export has a TSDoc `@example` block, so IDEs and LLM agents see usage at the call site via the bundled `.d.ts`. The package also ships `llms.txt` (compact index) and `llms-full.txt` (every signature plus an example) at the package root, following the [llmstxt.org](https://llmstxt.org) convention.
