Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ jobs:
- name: Installing dependencies
run: pnpm install --frozen-lockfile

- name: Verify registry signatures for installed packages
run: pnpm audit signatures
- name: Building
run: pnpm build

- name: Install npm for Trusted Publishing
run: pnpm add --global npm@latest

- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
run: pnpm exec semantic-release
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"commitlint": "^21.2.3",
"commitlint-plugin-cspell": "^0.9.5",
"conventional-changelog-conventionalcommits": "^9.3.1",
"cspell": "^10.3.4",
"cspell": "^10.3.5",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.5",
Expand Down
379 changes: 189 additions & 190 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

18 changes: 2 additions & 16 deletions src/get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getNode } from './internal/getNode.js';
import { pathParts } from './pathParts.js';

import type { Path, PathValue } from './types/objects.js';
Expand All @@ -23,20 +24,5 @@ export function get<Input extends object, InputPath extends Path<Input> | Proper
input: Input,
path: InputPath,
): unknown {
if (path === '') {
return input;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = input;

for (const part of pathParts(path)) {
if (typeof current === 'object' && current !== null && part in current) {
current = current[part];
} else {
return;
}
}

return current;
return path === '' ? input : getNode(input, pathParts(path)).value;
}
20 changes: 3 additions & 17 deletions src/has.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getNode } from './internal/getNode.js';
import { pathParts } from './pathParts.js';
import { isAnyObject } from './predicate/isAnyObject.js';

Expand Down Expand Up @@ -31,24 +32,9 @@ export function has<Input extends object, InputPath extends Path<Input> | string
input: Input,
path: InputPath,
): input is Input & FromPath<InputPath, IfNever<PathValue<Input, InputPath>, unknown, PathValue<Input, InputPath>>> {
if (!isAnyObject(input)) {
if (!isAnyObject(input) || path === '') {
return false;
}

if (path === '') {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = input;

for (const part of pathParts(path)) {
if (typeof current === 'object' && current !== null && part in current) {
current = current[part];
} else {
return false;
}
}

return true;
return getNode(input, pathParts(path)).found;
}
44 changes: 44 additions & 0 deletions src/internal/getNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { isAnyObjectWith } from '../predicate/isAnyObjectWith.js';

export type GetNodeResult =
| {
value: unknown;
found: true;
visitedNodes: PropertyKey[];
}
| {
value: undefined;
found: false;
visitedNodes: PropertyKey[];
};

/**
* Retrieves the value of a nested property from an object based on a sequence of keys.
*
* @internal
*/
export function getNode<Input extends object>(input: Input, pathKeys: Iterable<PropertyKey>): GetNodeResult {
let current: unknown = input;

const visitedNodes: PropertyKey[] = [];

for (const key of pathKeys) {
visitedNodes.push(key);

if (isAnyObjectWith(current, key)) {
current = current[key];
} else {
return {
value: undefined,
found: false,
visitedNodes,
};
}
}

return {
value: current,
found: true,
visitedNodes,
};
}
1 change: 1 addition & 0 deletions src/predicate/factory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './nullable.js';
export * from './nullish.js';
export * from './optional.js';
export * from './range.js';
export * from './record.js';
export * from './shape.js';
export * from './simple.js';
export * from './stringLength.js';
Expand Down
13 changes: 13 additions & 0 deletions src/predicate/factory/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isObject } from '../isObject.js';

import type { TypePredicateFn } from '../../types/functions.js';

export const record = <Key extends string | symbol, Value>(
keyPredicate: TypePredicateFn<Key>,
valuePredicate: TypePredicateFn<Value>,
): TypePredicateFn<Record<Key, Value>> => {
return (input: unknown): input is Record<Key, Value> =>
isObject(input)
? Reflect.ownKeys(input).every((key) => keyPredicate(key) && valuePredicate(Reflect.get(input, key)))
: false;
};
14 changes: 11 additions & 3 deletions src/predicate/isAnyObjectWith.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { isAnyObject } from './isAnyObject.js';

/**
* Determine if `input` is an object and has the provided properties.
* Determine if `input` is an object and has the provided property or properties.
*
* @example
* ```ts
* isAnyObjectWith({ a: 1 }, 'a'); // true
* isAnyObjectWith({ a: 1 }, 'b'); // false
* isAnyObjectWith({ a: 1, b: 2 }, ['a', 'b']); // true
* isAnyObjectWith({ a: 1 }, ['a', 'b']); // false
* isAnyObjectWith({}, 'toString'); // true (inherited properties are included)
* isAnyObjectWith(['a'], 0); // true (arrays are objects)
* isAnyObjectWith(null, 'a'); // false
* ```
*/
export const isAnyObjectWith = <T, P extends PropertyKey>(
input: T,
properties: P[],
properties: P | P[],
): input is T & Record<P, unknown> => {
return isAnyObject(input) && properties.every((property) => property in input);
return (
isAnyObject(input) &&
(Array.isArray(properties) ? properties.every((property) => property in input) : properties in input)
);
};
22 changes: 8 additions & 14 deletions src/set.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { assertIsObject } from './assertion/assertIsObject.js';
import { getNode } from './internal/getNode.js';
import { pathParts } from './pathParts.js';

import type { Path, PathValue } from './types/objects.js';
Expand Down Expand Up @@ -48,21 +50,13 @@ export function set<Input extends object, InputPath extends Path<Input> | string
throw new Error('Path must have at least one part');
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = input;
const result = getNode(input, parts);

const useParts: PropertyKey[] = [];

// get a reference to the last node that will be updated
for (const part of parts) {
useParts.push(part);

if (typeof current === 'object' && current !== null && part in current) {
current = current[part];
} else {
throw new Error(`Path "${useParts.join('.')}" does not exist in the input object`);
}
if (!result.found) {
throw new Error(`Path "${result.visitedNodes.join('.')}" does not exist in the input object`);
}

return (current[lastPart] = value);
assertIsObject(result.value);

return (result.value[lastPart] = value);
}
Loading
Loading