├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── license ├── package.json ├── readme.md ├── source ├── array-includes.ts ├── as-writable.ts ├── assert-defined.ts ├── assert-error.ts ├── assert-present.ts ├── index.ts ├── is-defined.ts ├── is-empty.ts ├── is-finite.ts ├── is-infinite.ts ├── is-integer.ts ├── is-present.ts ├── is-safe-integer.ts ├── object-entries.ts ├── object-from-entries.ts ├── object-has-own.ts ├── object-keys.ts ├── safe-cast-to.ts └── set-has.ts ├── test ├── array-includes.ts ├── as-writable.ts ├── assert-defined.ts ├── assert-error.ts ├── assert-present.ts ├── is-defined.ts ├── is-empty.ts ├── is-infinite.ts ├── is-present.ts ├── object-entries.ts ├── object-from-entries.ts ├── object-has-own.ts ├── object-keys.ts ├── safe-cast-to.ts ├── set-has.ts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | distribution 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-extras", 3 | "version": "0.14.0", 4 | "description": "Essential utilities for TypeScript projects", 5 | "license": "MIT", 6 | "repository": "sindresorhus/ts-extras", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./distribution/index.d.ts", 16 | "default": "./distribution/index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "build": "del-cli distribution && tsc", 24 | "test": "xo && ava && tsc --project test && npm run build", 25 | "prepack": "npm run build" 26 | }, 27 | "files": [ 28 | "distribution" 29 | ], 30 | "keywords": [ 31 | "typescript", 32 | "ts", 33 | "type", 34 | "types", 35 | "utility", 36 | "utilities", 37 | "util", 38 | "extra", 39 | "extras", 40 | "guard", 41 | "guards", 42 | "helpers" 43 | ], 44 | "dependencies": { 45 | "type-fest": "^4.39.1" 46 | }, 47 | "devDependencies": { 48 | "@sindresorhus/tsconfig": "^7.0.0", 49 | "ava": "^6.2.0", 50 | "del-cli": "^6.0.0", 51 | "expect-type": "^1.2.1", 52 | "tsx": "^4.19.3", 53 | "typescript": "^5.8.3", 54 | "xo": "^0.60.0" 55 | }, 56 | "ava": { 57 | "extensions": { 58 | "ts": "module" 59 | }, 60 | "nodeArguments": [ 61 | "--import=tsx/esm" 62 | ], 63 | "workerThreads": false 64 | }, 65 | "xo": { 66 | "rules": { 67 | "@typescript-eslint/ban-ts-comment": "off", 68 | "@typescript-eslint/naming-convention": "off" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ts-extras [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://giphy.com/gifs/illustration-rainbow-unicorn-26AHG5KGFxSkUWw1i) [![npm dependents](https://badgen.net/npm/dependents/ts-extras)](https://www.npmjs.com/package/ts-extras?activeTab=dependents) [![npm downloads](https://badgen.net/npm/dt/ts-extras)](https://www.npmjs.com/package/ts-extras) 2 | 3 | > Essential utilities for TypeScript projects 4 | 5 | *Ideas for additional **essential** utilities welcome. Type-only utilities belong in [type-fest](https://github.com/sindresorhus/type-fest).* 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install ts-extras 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import {isDefined} from 'ts-extras'; 17 | 18 | [1, undefined, 2].filter(isDefined); 19 | //=> [1, 2] 20 | ``` 21 | 22 | ## API 23 | 24 | **General** 25 | 26 | - [`asWritable`](source/as-writable.ts) - Cast the given value to be [`Writable`](https://github.com/sindresorhus/type-fest/blob/main/source/writable.d.ts). 27 | - [`safeCastTo`](source/safe-cast-to.ts) - Cast a value to the given type safely. 28 | 29 | **Type guard** 30 | 31 | - [`isDefined`](source/is-defined.ts) - Check whether a value is defined (not `undefined`). 32 | - [`isPresent`](source/is-present.ts) - Check whether a value is present (not `null` or `undefined`). 33 | - [`isEmpty`](source/is-empty.ts) - Check whether an array is empty. 34 | - [`isInfinite`](source/is-infinite.ts) - Check whether a value is infinite. 35 | - [`assertDefined`](source/assert-defined.ts) - Assert that the given value is defined, meaning it is not `undefined`. 36 | - [`assertPresent`](source/assert-present.ts) - Assert that the given value is present (non-nullable), meaning it is neither `null` or `undefined`. 37 | - [`assertError`](source/assert-error.ts) - Assert that the given value is an `Error`. 38 | 39 | **Improved builtin** 40 | 41 | - [`arrayIncludes`](source/array-includes.ts) - An alternative to `Array#includes()` that properly acts as a type guard. 42 | - [`setHas`](source/set-has.ts) - An alternative to `Set#has()` that properly acts as a type guard. 43 | - [`objectKeys`](source/object-keys.ts) - A strongly-typed version of `Object.keys()`. 44 | - [`objectEntries`](source/object-entries.ts) - A strongly-typed version of `Object.entries()`. 45 | - [`objectFromEntries`](source/object-from-entries.ts) - A strongly-typed version of `Object.fromEntries()`. 46 | - [`objectHasOwn`](source/object-has-own.ts) - A strongly-typed version of `Object.hasOwn()`. 47 | - [`isFinite`](source/is-finite.ts) - A strongly-typed version of `Number.isFinite()`. 48 | - [`isInteger`](source/is-integer.ts) - A strongly-typed version of `Number.isInteger()`. 49 | - [`isSafeInteger`](source/is-safe-integer.ts) - A strongly-typed version of `Number.isSafeInteger()`. 50 | 51 | ## FAQ 52 | 53 | #### What is the difference between this and `type-fest`? 54 | 55 | The `type-fest` package contains only types, meaning they are only used at compile-time and nothing is ever compiled into actual JavaScript code. This package contains functions that are compiled into JavaScript code and used at runtime. 56 | 57 | ## Related 58 | 59 | - [type-fest](https://github.com/sindresorhus/type-fest) - A collection of essential TypeScript types 60 | - [is](https://github.com/sindresorhus/is) - Type guards for any situation 61 | -------------------------------------------------------------------------------- /source/array-includes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | An alternative to `Array#includes()` that properly acts as a type guard. 3 | 4 | It was [rejected](https://github.com/microsoft/TypeScript/issues/26255#issuecomment-748211891) from being done in TypeScript itself. 5 | 6 | @example 7 | ``` 8 | import {arrayIncludes} from 'ts-extras'; 9 | 10 | const values = ['a', 'b', 'c'] as const; 11 | const valueToCheck: unknown = 'a'; 12 | 13 | if (arrayIncludes(values, valueToCheck)) { 14 | // We now know that the value is of type `typeof values[number]`. 15 | } 16 | ``` 17 | 18 | @category Improved builtin 19 | @category Type guard 20 | */ 21 | export function arrayIncludes( 22 | array: Type[] | readonly Type[], 23 | item: SuperType, 24 | fromIndex?: number, 25 | ): item is Type { 26 | return array.includes(item as Type, fromIndex); 27 | } 28 | -------------------------------------------------------------------------------- /source/as-writable.ts: -------------------------------------------------------------------------------- 1 | import {type Writable} from 'type-fest'; 2 | 3 | /** 4 | Cast the given value to be [`Writable`](https://github.com/sindresorhus/type-fest/blob/main/source/writable.d.ts). 5 | 6 | This is useful because of a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/45618#issuecomment-908072756). 7 | 8 | @example 9 | ``` 10 | import {asWritable} from 'ts-extras'; 11 | 12 | const writableContext = asWritable((await import('x')).context); 13 | ``` 14 | 15 | @category General 16 | */ 17 | export function asWritable(value: T): Writable { 18 | return value as any; // eslint-disable-line @typescript-eslint/no-unsafe-return 19 | } 20 | -------------------------------------------------------------------------------- /source/assert-defined.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from './is-defined.js'; 2 | 3 | /** 4 | Assert that the given value is defined, meaning it is not `undefined`. 5 | 6 | If the value is `undefined`, a helpful `TypeError` will be thrown. 7 | 8 | @example 9 | ``` 10 | import {assertDefined} from 'ts-extras'; 11 | 12 | const unicorn = 'unicorn'; 13 | assertDefined(unicorn); 14 | 15 | const notUnicorn = undefined; 16 | assertDefined(notUnicorn); 17 | //=> TypeError: Expected a defined value, got `undefined` 18 | ``` 19 | 20 | @category Type guard 21 | */ 22 | export function assertDefined(value: T | undefined): asserts value is T { 23 | if (!isDefined(value)) { 24 | throw new TypeError('Expected a defined value, got `undefined`'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/assert-error.ts: -------------------------------------------------------------------------------- 1 | const {toString} = Object.prototype; 2 | 3 | /** 4 | Assert that the given value is an `Error`. 5 | 6 | If the value is not an `Error`, a helpful `TypeError` will be thrown. 7 | 8 | This can be useful as any value could potentially be thrown, but in practice, it's always an `Error`. However, because of this, TypeScript makes the caught error in a try/catch statement `unknown`, which is inconvenient to deal with. 9 | 10 | @example 11 | ``` 12 | import {assertError} from 'ts-extras'; 13 | 14 | try { 15 | fetchUnicorns(); 16 | } catch (error: unknown) { 17 | assertError(error); 18 | 19 | // `error` is now of type `Error` 20 | 21 | if (error.message === 'Failed to fetch') { 22 | retry(); 23 | return; 24 | } 25 | 26 | throw error; 27 | } 28 | ``` 29 | 30 | @category Type guard 31 | */ 32 | export function assertError(value: unknown): asserts value is Error { 33 | if (!(value instanceof Error || toString.call(value) === '[object Error]')) { 34 | throw new TypeError(`Expected an \`Error\`, got \`${JSON.stringify(value)}\` (${typeof value})`); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/assert-present.ts: -------------------------------------------------------------------------------- 1 | import {isPresent} from './is-present.js'; 2 | 3 | /** 4 | Assert that the given value is present (non-nullable), meaning it is neither `null` or `undefined`. 5 | 6 | If the value is not present (`undefined` or `null`), a helpful `TypeError` will be thrown. 7 | 8 | @example 9 | ``` 10 | import {assertPresent} from 'ts-extras'; 11 | 12 | const unicorn = 'unicorn'; 13 | assertPresent(unicorn); 14 | 15 | const notUnicorn = null; 16 | assertPresent(notUnicorn); 17 | //=> TypeError: Expected a present value, got `null` 18 | ``` 19 | 20 | @category Type guard 21 | */ 22 | export function assertPresent(value: T): asserts value is NonNullable { 23 | if (!isPresent(value)) { 24 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 25 | throw new TypeError(`Expected a present value, got \`${value}\``); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | export {safeCastTo} from './safe-cast-to.js'; 2 | export {arrayIncludes} from './array-includes.js'; 3 | export {asWritable} from './as-writable.js'; 4 | export {assertDefined} from './assert-defined.js'; 5 | export {assertError} from './assert-error.js'; 6 | export {assertPresent} from './assert-present.js'; 7 | export {isDefined} from './is-defined.js'; 8 | export {isPresent} from './is-present.js'; 9 | export {isEmpty} from './is-empty.js'; 10 | export {isFinite} from './is-finite.js'; 11 | export {isInfinite} from './is-infinite.js'; 12 | export {isInteger} from './is-integer.js'; 13 | export {isSafeInteger} from './is-safe-integer.js'; 14 | export {objectEntries} from './object-entries.js'; 15 | export {objectFromEntries} from './object-from-entries.js'; 16 | export {objectHasOwn} from './object-has-own.js'; 17 | export {objectKeys} from './object-keys.js'; 18 | export {setHas} from './set-has.js'; 19 | -------------------------------------------------------------------------------- /source/is-defined.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Check whether a value is defined, meaning it is not `undefined`. 3 | 4 | This can be useful as a type guard, as for example, `[1, undefined].filter(Boolean)` does not always type-guard correctly. 5 | 6 | @example 7 | ``` 8 | import {isDefined} from 'ts-extras'; 9 | 10 | [1, undefined, 2].filter(isDefined); 11 | //=> [1, 2] 12 | ``` 13 | 14 | @category Type guard 15 | */ 16 | export function isDefined(value: T | undefined): value is T { 17 | return value !== undefined; 18 | } 19 | -------------------------------------------------------------------------------- /source/is-empty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Check whether an array is empty. 3 | 4 | This is useful because doing `array.length === 0` on its own won't work as a type-guard. 5 | 6 | @example 7 | ``` 8 | import {isEmpty} from 'ts-extras'; 9 | 10 | isEmpty([1, 2, 3]); 11 | //=> false 12 | 13 | isEmpty([]); 14 | //=> true 15 | ``` 16 | 17 | @category Type guard 18 | */ 19 | /* eslint-disable @typescript-eslint/ban-types */ 20 | export function isEmpty(array: readonly unknown[]): array is readonly []; 21 | export function isEmpty(array: unknown[]): array is []; 22 | export function isEmpty(array: readonly unknown[]): array is [] | readonly [] { 23 | return array.length === 0; 24 | } 25 | -------------------------------------------------------------------------------- /source/is-finite.ts: -------------------------------------------------------------------------------- 1 | import {type Finite} from 'type-fest'; 2 | 3 | /** 4 | An alternative to `Number.isFinite()` that properly acts as a type guard. 5 | 6 | @category Improved builtin 7 | @category Type guard 8 | */ 9 | export const isFinite = Number.isFinite as (value: T) => value is Finite; 10 | -------------------------------------------------------------------------------- /source/is-infinite.ts: -------------------------------------------------------------------------------- 1 | import {type NegativeInfinity, type PositiveInfinity} from 'type-fest'; 2 | 3 | /** 4 | Check whether a value is infinite. 5 | 6 | @category Type guard 7 | */ 8 | export function isInfinite(value: unknown): value is NegativeInfinity | PositiveInfinity { 9 | return !Number.isNaN(value) && !Number.isFinite(value); 10 | } 11 | -------------------------------------------------------------------------------- /source/is-integer.ts: -------------------------------------------------------------------------------- 1 | import {type Integer} from 'type-fest'; 2 | 3 | /** 4 | An alternative to `Number.isInteger()` that properly acts as a type guard. 5 | 6 | @category Improved builtin 7 | @category Type guard 8 | */ 9 | export const isInteger = Number.isInteger as (value: T) => value is Integer; 10 | -------------------------------------------------------------------------------- /source/is-present.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Check whether a value is present (non-nullable), meaning it is neither `null` or `undefined`. 3 | 4 | This can be useful as a type guard, as for example, `[1, null].filter(Boolean)` does not always type-guard correctly. 5 | 6 | @example 7 | ``` 8 | import {isPresent} from 'ts-extras'; 9 | 10 | [1, null, 2, undefined].filter(isPresent); 11 | //=> [1, 2] 12 | ``` 13 | 14 | @category Type guard 15 | */ 16 | export function isPresent(value: T): value is NonNullable { 17 | return value !== null && value !== undefined; 18 | } 19 | -------------------------------------------------------------------------------- /source/is-safe-integer.ts: -------------------------------------------------------------------------------- 1 | import {type Integer} from 'type-fest'; 2 | 3 | /** 4 | An alternative to `Number.isSafeInteger()` that properly acts as a type guard. 5 | 6 | @category Improved builtin 7 | @category Type guard 8 | */ 9 | export const isSafeInteger = Number.isSafeInteger as (value: T) => value is Integer; 10 | -------------------------------------------------------------------------------- /source/object-entries.ts: -------------------------------------------------------------------------------- 1 | import {type ObjectKeys} from './object-keys.js'; 2 | 3 | /** 4 | A strongly-typed version of `Object.entries()`. 5 | 6 | This is useful since `Object.entries()` always returns an array of `Array<[string, T]>`. This function returns a strongly-typed array of the entries of the given object. 7 | 8 | - [TypeScript issues about this](https://github.com/microsoft/TypeScript/pull/12253) 9 | 10 | @example 11 | ``` 12 | import {objectEntries} from 'ts-extras'; 13 | 14 | const stronglyTypedEntries = objectEntries({a: 1, b: 2, c: 3}); 15 | //=> Array<['a' | 'b' | 'c', number]> 16 | 17 | const untypedEntries = Object.entries(items); 18 | //=> Array<[string, number]> 19 | ``` 20 | 21 | @category Improved builtin 22 | @category Type guard 23 | */ 24 | export const objectEntries = Object.entries as >(value: Type) => Array<[ObjectKeys, Required[ObjectKeys]]>; 25 | -------------------------------------------------------------------------------- /source/object-from-entries.ts: -------------------------------------------------------------------------------- 1 | /** 2 | A strongly-typed version of `Object.fromEntries()`. 3 | 4 | This is useful since `Object.fromEntries()` always returns `{[key: string]: T}`. This function returns a strongly-typed object from the given array of entries. 5 | 6 | - [TypeScript issues about this](https://github.com/microsoft/TypeScript/issues/35745) 7 | 8 | @example 9 | ``` 10 | import {objectFromEntries} from 'ts-extras'; 11 | 12 | const stronglyTypedObjectFromEntries = objectFromEntries([ 13 | ['a', 123], 14 | ['b', 'someString'], 15 | ['c', true], 16 | ]); 17 | //=> {a: number; b: string; c: boolean} 18 | 19 | const untypedEntries = Object.fromEntries(entries); 20 | //=> {[key: string]: string} 21 | ``` 22 | 23 | @category Improved builtin 24 | @category Type guard 25 | */ 26 | export const objectFromEntries = Object.fromEntries as >(values: Entries) => { 27 | [K in Extract[0]]: Extract[1] 28 | }; 29 | -------------------------------------------------------------------------------- /source/object-has-own.ts: -------------------------------------------------------------------------------- 1 | const has = Object.prototype.hasOwnProperty; 2 | 3 | /** 4 | A strongly-typed version of `Object.hasOwn()`. 5 | 6 | Returns a boolean indicating whether the given object has the given property as its own property. 7 | 8 | @example 9 | ``` 10 | import {objectHasOwn} from 'ts-extras'; 11 | 12 | objectHasOwn({}, 'hello'); 13 | //=> false 14 | 15 | objectHasOwn([1, 2, 3], 0); 16 | //=> true 17 | ``` 18 | 19 | @category Improved builtin 20 | @category Type guard 21 | */ 22 | export function objectHasOwn( 23 | object: ObjectType, 24 | key: Key, 25 | ): object is (ObjectType & Record) { 26 | // TODO: Use `Object.hasOwn()` when targeting Node.js 16. 27 | return has.call(object, key); 28 | } 29 | -------------------------------------------------------------------------------- /source/object-keys.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | 3 | export type ObjectKeys = `${Exclude}`; 4 | 5 | /** 6 | A strongly-typed version of `Object.keys()`. 7 | 8 | This is useful since `Object.keys()` always returns an array of strings. This function returns a strongly-typed array of the keys of the given object. 9 | 10 | - [Explanation](https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript) 11 | - [TypeScript issues about this](https://github.com/microsoft/TypeScript/issues/45390) 12 | 13 | @example 14 | ``` 15 | import {objectKeys} from 'ts-extras'; 16 | 17 | const stronglyTypedItems = objectKeys({a: 1, b: 2, c: 3}); // => Array<'a' | 'b' | 'c'> 18 | const untypedItems = Object.keys(items); // => Array 19 | ``` 20 | 21 | @category Improved builtin 22 | @category Type guard 23 | */ 24 | export const objectKeys = Object.keys as (value: Type) => Array>; 25 | -------------------------------------------------------------------------------- /source/safe-cast-to.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Cast a value to the given type safely. 3 | 4 | The `as` keyword allows unsafe conversions between unrelated types, like converting a number to a string. This function restricts casting to compatible types, preserving type safety. 5 | 6 | @example 7 | ``` 8 | type Foo = { 9 | a: string; 10 | b?: number; 11 | }; 12 | 13 | declare const possibleUndefined: Foo | undefined; 14 | 15 | const foo = possibleUndefined ?? safeCastTo>({}); 16 | console.log(foo.a ?? '', foo.b ?? 0); 17 | 18 | const bar = possibleUndefined ?? {}; 19 | // @ts-expect-error 20 | console.log(bar.a ?? '', bar.b ?? 0); 21 | // ^^^ Property 'a' does not exist on type '{}'.(2339) 22 | // ^^^ Property 'b' does not exist on type '{}'.(2339) 23 | ``` 24 | 25 | @category General 26 | */ 27 | export function safeCastTo(value: T): T { 28 | return value; 29 | } 30 | -------------------------------------------------------------------------------- /source/set-has.ts: -------------------------------------------------------------------------------- 1 | /** 2 | An alternative to `Set#has()` that properly acts as a type guard. 3 | 4 | It was [rejected](https://github.com/microsoft/TypeScript/issues/42641#issuecomment-774168319) from being done in TypeScript itself. 5 | 6 | @example 7 | ``` 8 | import {setHas} from 'ts-extras'; 9 | 10 | const values = ['a', 'b', 'c'] as const; 11 | const valueSet = new Set(values); 12 | const valueToCheck: unknown = 'a'; 13 | 14 | if (setHas(valueSet, valueToCheck)) { 15 | // We now know that the value is of type `typeof values[number]`. 16 | } 17 | ``` 18 | 19 | @category Improved builtin 20 | @category Type guard 21 | */ 22 | export function setHas( 23 | set: ReadonlySet, 24 | item: SuperType, 25 | ): item is Type { 26 | return set.has(item as Type); 27 | } 28 | -------------------------------------------------------------------------------- /test/array-includes.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {arrayIncludes} from '../source/index.js'; 4 | 5 | test('arrayIncludes()', t => { 6 | const values = ['a', 'b', 'c'] as const; 7 | const validValue: unknown = 'a'; 8 | const invalidValue: unknown = 'd'; 9 | const invalidTypedValue = 1; 10 | let testValueType: typeof values[number]; 11 | 12 | expectTypeOf(values).items.toMatchTypeOf(); 13 | expectTypeOf(values).items.toMatchTypeOf(); 14 | expectTypeOf(values).items.not.toMatchTypeOf(); 15 | 16 | t.true(arrayIncludes(values, validValue)); 17 | t.false(arrayIncludes(values, invalidValue)); 18 | // @ts-expect-error 19 | t.false(arrayIncludes(values, invalidTypedValue)); 20 | 21 | if (arrayIncludes(values, validValue)) { 22 | // @ts-expect-error 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 24 | values.push(); // Ensure readonly array is still readonly. 25 | 26 | testValueType = validValue; 27 | } else { 28 | // @ts-expect-error 29 | testValueType = validValue; 30 | 31 | // @ts-expect-error 32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 33 | values.push(); // Ensure readonly array is still readonly. 34 | } 35 | 36 | t.is(testValueType, 'a'); 37 | }); 38 | -------------------------------------------------------------------------------- /test/as-writable.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {asWritable} from '../source/index.js'; 3 | 4 | test('asWritable()', t => { 5 | type Fixture = { 6 | readonly a: number; 7 | }; 8 | 9 | const fixture: Fixture = {a: 1}; 10 | 11 | // @ts-expect-error 12 | fixture.a = 2; 13 | 14 | const writableFixture = asWritable(fixture); 15 | writableFixture.a = 2; 16 | t.is(writableFixture.a, 2); 17 | }); 18 | -------------------------------------------------------------------------------- /test/assert-defined.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {assertDefined} from '../source/index.js'; 4 | 5 | test('assertDefined()', t => { 6 | t.notThrows(() => { 7 | assertDefined('defined'); 8 | }); 9 | 10 | t.throws(() => { 11 | assertDefined(undefined); 12 | }, { 13 | instanceOf: TypeError, 14 | }); 15 | 16 | const maybeDefined = 'defined' as string | undefined; 17 | assertDefined(maybeDefined); 18 | expectTypeOf(maybeDefined).toMatchTypeOf(); 19 | }); 20 | -------------------------------------------------------------------------------- /test/assert-error.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {assertError} from '../source/index.js'; 3 | 4 | test('assertError()', t => { 5 | t.notThrows(() => { 6 | assertError(new Error('x')); 7 | }); 8 | 9 | t.throws(() => { 10 | assertError('x'); 11 | }, { 12 | instanceOf: TypeError, 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/assert-present.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {assertPresent} from '../source/index.js'; 4 | 5 | test('assertPresent()', t => { 6 | t.notThrows(() => { 7 | assertPresent('present'); 8 | }); 9 | 10 | t.throws(() => { 11 | assertPresent(null); 12 | }, { 13 | instanceOf: TypeError, 14 | }); 15 | 16 | // eslint-disable-next-line @typescript-eslint/ban-types 17 | const maybePresent = 'present' as string | undefined | null; 18 | assertPresent(maybePresent); 19 | expectTypeOf(maybePresent).toMatchTypeOf(); 20 | }); 21 | -------------------------------------------------------------------------------- /test/is-defined.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {isDefined} from '../source/index.js'; 4 | 5 | test('isDefined()', t => { 6 | t.false(isDefined(undefined)); 7 | t.true(isDefined(null)); 8 | t.true(isDefined(1)); 9 | t.true(isDefined('x')); 10 | 11 | const fixture = [1, undefined].filter(x => isDefined(x)); 12 | expectTypeOf(fixture).not.toBeUndefined(); 13 | 14 | const number: number | undefined = 1; 15 | 16 | if (isDefined(number)) { 17 | expectTypeOf(number).toBeNumber(); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /test/is-empty.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {isEmpty} from '../source/index.js'; 3 | 4 | test('isEmpty()', t => { 5 | t.false(isEmpty([1, 2, 3])); 6 | t.true(isEmpty([])); 7 | 8 | const immutable: readonly number[] = [1, 2, 3]; 9 | 10 | if (isEmpty(immutable)) { 11 | // @ts-expect-error Should not change immutability 12 | immutable.pop(); // eslint-disable-line @typescript-eslint/no-unsafe-call 13 | } 14 | 15 | const mutable = [1, 2, 3]; 16 | 17 | if (!isEmpty(mutable)) { 18 | mutable.pop(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/is-infinite.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {type NegativeInfinity, type PositiveInfinity} from 'type-fest'; 4 | import {isInfinite} from '../source/index.js'; 5 | 6 | test('isInfinite()', t => { 7 | t.false(isInfinite(123)); 8 | t.true(isInfinite(Number.POSITIVE_INFINITY)); 9 | t.true(isInfinite(Number.NEGATIVE_INFINITY)); 10 | 11 | const number_ = 123 as number; 12 | 13 | if (isInfinite(number_)) { 14 | expectTypeOf(number_); 15 | } else { 16 | expectTypeOf(number_); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /test/is-present.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {isPresent} from '../source/index.js'; 4 | 5 | test('isPresent()', t => { 6 | t.false(isPresent(null)); 7 | t.false(isPresent(undefined)); 8 | t.true(isPresent(1)); 9 | t.true(isPresent('x')); 10 | 11 | const fixture = [1, null].filter(x => isPresent(x)); 12 | expectTypeOf(fixture).not.toBeNullable(); 13 | 14 | // eslint-disable-next-line @typescript-eslint/ban-types 15 | const nullable: null | undefined = null; 16 | 17 | if (isPresent(nullable)) { 18 | expectTypeOf(nullable).toBeNever(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/object-entries.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {objectEntries} from '../source/index.js'; 4 | 5 | test('objectEntries()', t => { 6 | type Entry = ['1' | 'stringKey', number | string]; 7 | 8 | const entries = objectEntries({ 9 | 1: 123, 10 | stringKey: 'someString', 11 | [Symbol('symbolKey')]: true, 12 | }); 13 | 14 | expectTypeOf(entries); 15 | t.deepEqual(entries, [['1', 123], ['stringKey', 'someString']]); 16 | }); 17 | 18 | // Optional property 19 | { 20 | type Foo = { 21 | a?: string; 22 | }; 23 | 24 | const x: Foo = {}; 25 | const entries = objectEntries(x); 26 | expectTypeOf>(entries); 27 | 28 | expectTypeOf<['a', string] | undefined>(entries[0]); 29 | } 30 | -------------------------------------------------------------------------------- /test/object-from-entries.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {objectFromEntries} from '../source/index.js'; 4 | 5 | test('objectFromEntries()', t => { 6 | type ObjectFromEntries = { 7 | [x: symbol]: boolean; 8 | 1: number; 9 | stringKey: string; 10 | }; 11 | 12 | const symbolKey = Symbol('symbolKey'); 13 | 14 | const objectFromEntries_ = objectFromEntries([ 15 | [1, 123], 16 | ['stringKey', 'someString'], 17 | [symbolKey, true], 18 | ]); 19 | 20 | expectTypeOf(objectFromEntries_); 21 | t.deepEqual(objectFromEntries_, { 22 | 1: 123, 23 | stringKey: 'someString', 24 | [symbolKey]: true, 25 | }); 26 | 27 | const objectFromReadonlyEntries_ = objectFromEntries([ 28 | [1, 123], 29 | ['stringKey', 'someString'], 30 | [symbolKey, true], 31 | ] as const); 32 | 33 | expectTypeOf(objectFromReadonlyEntries_); 34 | t.deepEqual(objectFromReadonlyEntries_, { 35 | 1: 123, 36 | stringKey: 'someString', 37 | [symbolKey]: true, 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/object-has-own.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {objectHasOwn} from '../source/index.js'; 4 | 5 | test('objectHasOwn()', t => { 6 | const symbolKey = Symbol('symbolKey'); 7 | 8 | const object_: unknown = { 9 | 1: 1, 10 | b: 2, 11 | [symbolKey]: 3, 12 | }; 13 | 14 | t.true(objectHasOwn(object_, 1)); 15 | t.true(objectHasOwn(object_, 'b')); 16 | t.true(objectHasOwn(object_, symbolKey)); 17 | t.false(objectHasOwn(object_, 'hello')); 18 | 19 | if (objectHasOwn(object_, 1) && objectHasOwn(object_, 'b') && objectHasOwn(object_, symbolKey)) { 20 | expectTypeOf<{ 21 | 1: unknown; 22 | b: unknown; 23 | [symbolKey]: unknown; 24 | }>(object_); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /test/object-keys.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {objectKeys} from '../source/index.js'; 4 | 5 | type TestInterface = { 6 | e: string; 7 | f: number; 8 | }; 9 | 10 | test('objectKeys()', t => { 11 | type Item = 'a' | 'b' | 'c' | '4'; 12 | 13 | const items = objectKeys({ 14 | a: 1, 15 | b: 2, 16 | c: 3, 17 | 4: 4, 18 | [Symbol('5')]: 5, 19 | }); 20 | 21 | expectTypeOf(items); 22 | t.deepEqual(items, ['4', 'a', 'b', 'c']); 23 | 24 | const interfaceInput: TestInterface = {e: 'a', f: 1}; 25 | const interfaceItems = objectKeys(interfaceInput); 26 | expectTypeOf>(interfaceItems); 27 | t.deepEqual(interfaceItems, ['e', 'f']); 28 | }); 29 | -------------------------------------------------------------------------------- /test/safe-cast-to.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import test from 'ava'; 3 | import {expectTypeOf} from 'expect-type'; 4 | import {safeCastTo} from '../source/index.js'; 5 | 6 | test('safeCastTo()', t => { 7 | type Foo = { 8 | a: string; 9 | b?: number; 10 | }; 11 | 12 | const EmptyObject = {}; 13 | const foo: Foo = { 14 | a: '', 15 | b: 0, 16 | }; 17 | 18 | t.is(EmptyObject, safeCastTo(EmptyObject)); 19 | t.is(foo, safeCastTo(foo)); 20 | 21 | expectTypeOf({}).toEqualTypeOf<{}>(); 22 | expectTypeOf(safeCastTo({})).toEqualTypeOf<{}>(); 23 | expectTypeOf({}).not.toEqualTypeOf>(); 24 | expectTypeOf(safeCastTo({})).not.toEqualTypeOf>(); 25 | expectTypeOf(safeCastTo>({})).toEqualTypeOf>(); 26 | expectTypeOf(safeCastTo>({}).a).toEqualTypeOf(); 27 | expectTypeOf(safeCastTo>({}).b).toEqualTypeOf(); 28 | 29 | expectTypeOf(foo).toEqualTypeOf(); 30 | expectTypeOf(safeCastTo(foo)).toEqualTypeOf(); 31 | expectTypeOf(safeCastTo>(foo)).not.toEqualTypeOf(); 32 | expectTypeOf(safeCastTo>(foo)).toEqualTypeOf>(); 33 | 34 | // @ts-expect-error 35 | safeCastTo({}); 36 | // @ts-expect-error 37 | safeCastTo>(foo); 38 | }); 39 | -------------------------------------------------------------------------------- /test/set-has.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import {setHas} from '../source/index.js'; 4 | 5 | test('setHas()', t => { 6 | const values = ['a', 'b', 'c'] as const; 7 | const valueSet = new Set(values); 8 | const validValue: unknown = 'a'; 9 | const invalidValue: unknown = 'd'; 10 | const invalidTypedValue = 1; 11 | let testValueType: typeof values[number]; 12 | 13 | expectTypeOf(values).items.toMatchTypeOf(); 14 | expectTypeOf(values).items.toMatchTypeOf(); 15 | expectTypeOf(values).items.not.toMatchTypeOf(); 16 | 17 | t.true(setHas(valueSet, validValue)); 18 | t.false(setHas(valueSet, invalidValue)); 19 | // @ts-expect-error 20 | t.false(setHas(valueSet, invalidTypedValue)); 21 | 22 | // eslint-disable-next-line unicorn/prefer-ternary 23 | if (setHas(valueSet, validValue)) { 24 | testValueType = validValue; 25 | } else { 26 | // @ts-expect-error 27 | testValueType = validValue; 28 | } 29 | 30 | t.is(testValueType, 'a'); 31 | }); 32 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "include": ["."], 4 | "compilerOptions": { 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "include": [ 4 | "source" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------