Skip to content

Commit

Permalink
Merge pull request #11 from clickbar/next
Browse files Browse the repository at this point in the history
v2
  • Loading branch information
djfhe authored Aug 18, 2024
2 parents e4f61fd + ed1097b commit 00dc0a8
Show file tree
Hide file tree
Showing 13 changed files with 3,995 additions and 2,991 deletions.
8 changes: 0 additions & 8 deletions .eslintrc.json

This file was deleted.

107 changes: 106 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,112 @@

All notable changes to **dot-diver** will be documented here. Inspired by [keep a changelog](https://keepachangelog.com/en/1.0.0/)

## Unreleased
## [2.0.0](https://github.com/clickbar/dot-diver/tree/2.0.0) (2024-08-18)

## Features

Support paths up to a depth of 40 (limited by TypeScript's recursion depth) by utilizing cyclic constraints to only lookup up
the next 3 (default) levels. This should
heavily improve performance and reduce the amount of generated types.

## Fixes

- partially fixes <https://github.com/clickbar/dot-diver/issues/2>
- <https://github.com/clickbar/dot-diver/issues/3>
- <https://github.com/clickbar/dot-diver/issues/4>
- <https://github.com/clickbar/dot-diver/issues/5>
- <https://github.com/clickbar/dot-diver/issues/30>
- <https://github.com/clickbar/dot-diver/issues/34>

### Breaking Changes

In general the `depth` limit of `Path` is no longer the max depth, but instead a 'lookahead' depth. E.g. if you have a depth limit of 3,
and are currently (while typing) at a depth of 2, the paths for the next 3 levels will be shown in autocompletion. You can no longer
constrain the max depth of the path, but only the lookahead depth. If this is a problem for you, please open an issue.

#### Value of Array Member, Optional Properties and Union Types

In 1.0.0 you received the full type for nested unions.

```typescript
type A = { a: number | { nested: string } }

type NestedType = PathValue<A, 'a.nested'> // string
```
This is incorrect behavior, because `a` could also be a number and not an object with a `nested` property. This is now fixed.
`PathValue` was split into two different types:
- `GetPathValue` for reading values
- `SetPathValue` for setting values
the resulting types differ when accessing values in an object or setting them.
```typescript
type A = {
a: number | { nested: string }
b: Record<string, number>
c: string[]
d?: { nested: string }
}

type NestedType1 = GetPathValue<A, 'a.nested'> // string | undefined
type NestedType2 = GetPathValue<A, 'b.key'> // number | undefined
type NestedType3 = GetPathValue<A, 'c.5'> // string | undefined
type NestedType4 = GetPathValue<A, 'd.nested'> // string | undefined

type NestedType5 = SetPathValue<A, 'a.nested', string> // string
type NestedType6 = SetPathValue<A, 'b.key', number> // number
type NestedType7 = SetPathValue<A, 'c.5', string> // string
type NestedType8 = SetPathValue<A, 'd.nested', string> // string
```
These type are used by their respective functions `getByPath` and `setByPath` and should more accurately represent the types of accessed values but can potentially break old code.
#### Custom Implementation
Implementation of custom `getByPath` and `setByPath` functions need to be updated for the new types.
If you implemented them to change the depth limit you might not need them anymore, as the (max) depth is now 40 (limited by TypeScript's recursion limit).
You still want to keep them, if you want to change the shown paths in autocompletion or have other use cases.
You only need to change the typing.
Old:
```typescript
function getByPathDepth5<T extends SearchableObject, P extends Path<T, 5> & string>(
object: T,
path: P,
): PathValue<T, P, 5> {
return getByPath(object, path) as PathValue<T, P, 5>
}

function setByPathDepth5<
T extends SearchableObject,
P extends Path<T, 5> & string,
V extends PathValue<T, P, 5>,
>(object: T, path: P, value: V): void {
setByPath(object, path, value as PathValue<T, P>)
}
```

New:

```typescript
function getByPathDepth5<T extends SearchableObject, P extends Path<T, P, { depth: 5 }> & string>(
object: T,
path: P,
): GetPathValue<T, P> {
return getByPath(object, path) as GetPathValue<T, P>
}

function setByPathDepth5<
T extends SearchableObject,
P extends Path<T, P, { onlyWriteable: true; depth: 5 }> & string,
>(object: T, path: P, value: SetPathValue<T, P>): void {
setByPath(object, path, value as SetPathValue<T, P>)
}
```

## [1.0.6](https://github.com/clickbar/dot-diver/tree/1.0.6) (2024-03-25)

Expand Down
113 changes: 72 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Dot Diver 🌊🔍

A lightweight, powerful, and dependency-free TypeScript utility library that provides types and functions to work with object paths in dot notation. Dive into your objects with ease, while maintaining comprehensive type safety! 🎉
A lightweight, powerful, dependency-free and heavily over engineered TypeScript utility library providing utility types and functions to work with object paths in dot notation.

Dot notation is a popular and convenient way to access deeply nested properties in objects. With Dot Diver, you can safely work with object paths in TypeScript projects, ensuring type correctness and productivity!
Dot notation is a popular and convenient way to access deeply nested properties in objects. With Dot Diver, you can safely work with object paths in TypeScript projects, ensuring complete type safety and avoiding runtime errors.

Example:

Expand Down Expand Up @@ -109,15 +109,18 @@ setByPath(object, 'f.1.g', 'new array-item-2')
console.log(object.f[1].g) // Output: 'new array-item-2'
```

> [!NOTE]
> At the moment, we can not support object properties having a '.' in their name, since this would conflict with the dot notation traversal.
<br>
<br>

### 🛣️ Path and 🔖 PathValue
### 🛣️ Path and 🔖 GetPathValue

<br>

```typescript
import type { Path, PathValue } from '@clickbar/dot-diver'
import type { Path, GetPathValue } from '@clickbar/dot-diver'

// Define a sample object type with nested properties
type MyObjectType = {
Expand All @@ -137,12 +140,12 @@ type MyObjectPaths = Path<MyObjectType>
// MyObjectPaths will be a union type representing all valid paths in dot notation:
// 'a' | 'b' | 'f' | 'b.c' | 'b.d' | 'b.d.e' | 'f.0' | 'f.1' | 'f.0.g' | 'f.1.g'

// Example 2: Using the PathValue type
type ValueAtPathA = PathValue<MyObjectType, 'a'> // Output: string
type ValueAtPathB_C = PathValue<MyObjectType, 'b.c'> // Output: number
type ValueAtPathB_D_E = PathValue<MyObjectType, 'b.d.e'> // Output: boolean
type ValueAtPathF_0 = PathValue<MyObjectType, 'f.0'> // Output: { g: string }
type ValueAtPathF_0_G = PathValue<MyObjectType, 'f.0.g'> // Output: string
// Example 2: Using the GetPathValue type
type ValueAtPathA = GetPathValue<MyObjectType, 'a'> // Output: string
type ValueAtPathB_C = GetPathValue<MyObjectType, 'b.c'> // Output: number
type ValueAtPathB_D_E = GetPathValue<MyObjectType, 'b.d.e'> // Output: boolean
type ValueAtPathF_0 = GetPathValue<MyObjectType, 'f.0'> // Output: { g: string }
type ValueAtPathF_0_G = GetPathValue<MyObjectType, 'f.0.g'> // Output: string
```
<br>
Expand All @@ -153,7 +156,7 @@ type ValueAtPathF_0_G = PathValue<MyObjectType, 'f.0.g'> // Output: string
<br>
```typescript
import type { Path, PathValue } from '@clickbar/dot-diver'
import type { Path, GetPathValue } from '@clickbar/dot-diver'

// Define an object type with nested properties and a cyclic dependency
type Node = {
Expand All @@ -163,57 +166,62 @@ type Node = {
children: Node[]
}

// Example 1: Using the Path type with a Depth limit
type NodePathsDepth2 = Path<Node, 2> // Depth limit of 2
// Example 1: Using the Path type with the default depth limit
type NodePathsDepth2 = Path<Node> // Depth limit of 2

// NodePathsDepth2 will be a union type representing all valid paths in dot notation up to a depth of 3:
// NodePathsDepth2 will be a union type representing all valid paths in dot notation up to a depth of 2:
// 'id' | 'label' | 'parent' | 'children' | 'parent.id' | 'parent.label' | 'parent.parent' | 'parent.children' | `parent.parent.${any}` | `parent.children.${any}` | `children.${number}` | `children.${number}.${any}`

// Example 2: Using the PathValue type with a Depth limit
type ValueAtPathParent_Id = PathValue<Node, 'parent.id', 3> // Output: number
type ValueAtPathChildren_0_Label = PathValue<Node, 'children.0.label', 3> // Output: string | undefined
type ValueAtPathParent_Parent_Parent = PathValue<Node, 'parent.parent.parent.parent', 3> // Output: unknown (due to the depth limit)
// Example 2: Using the Path type with a custom depth limit
type NodePathsDepth3 = Path<Node, never, { depth: 3; onlyWritable: false }> // Depth limit of 3

// With a depth limit of 3, NodePathsDepth3 will be a union type representing all valid paths in dot notation up to a depth of 3:
// 'id' | 'label' | 'parent' | 'children'
// | 'parent.id' | 'parent.label' | 'parent.parent' | 'parent.children' | `parent.parent.parent'
// | `parent.parent.parent' | 'parent.parent.children' | ... etc.
```

The default depth is currently **10**.\
At the moment, it is not possible to customize it when using the provided `getByPath` and `setByPath` functions.
This is further explained in this [issue](https://github.com/clickbar/dot-diver/issues/1).
The second parameter is an `offset`. You can provide a valid path to start the autocompletion from there.\
This is used in `getByPath` and `setByPath` to provide autocompletion for the next levels, starting from the current path.
When using `getByPath` and `setByPath`, the `Depth` parameter is the lookahead depth and not the max depth.

The default depth is currently **3**.

<br>
<br>

### ⚙️ Customizing the Depth Limit
### ⚙️ Customizing the Depth Lookahead Limit

You can still customize it, by implementing your own functions, which just calls ours.
Example:
You can customize the set and get functions, by implementing your own variant and using the provided types.\

Here is an example where we customize the lookahead depth to 5:

<br>

```typescript
import { getByPath, setByPath } from '@clickbar/dot-diver'

import type { Path, SearchableObject, PathValue } from '@clickbar/dot-diver'
import type { Path, SearchableObject, GetPathValue, SetPathValue } from '@clickbar/dot-diver'

function getByPathDepth5<T extends SearchableObject, P extends Path<T, 5> & string>(
function getByPathDepth5<T extends SearchableObject, P extends Path<T, P, { depth: 5 }> & string>(
object: T,
path: P,
): PathValue<T, P, 5> {
return getByPath(object, path) as PathValue<T, P, 5>
): GetPathValue<T, P> {
return getByPath(object, path) as GetPathValue<T, P>
}

function setByPathDepth5<
T extends SearchableObject,
P extends Path<T, 5> & string,
V extends PathValue<T, P, 5>,
>(object: T, path: P, value: V): void {
setByPath(object, path, value as PathValue<T, P>)
P extends Path<T, P, { onlyWriteable: true; depth: 5 }> & string,
>(object: T, path: P, value: SetPathValue<T, P>): void {
setByPath(object, path, value as SetPathValue<T, P>)
}

export { getByPathDepth5 as getByPath, setByPathDepth5 as setByPath }
```

The intersection between `Path<T, 5>` and `string` is necessary for TypeScript to successfully narrow down the type of `P` based on the user-provided `path` input.
Without the intersection, the `path` would just be of type `Path<T, 5>` and `PathValueEntry` would be a union of all possible return types.
The intersection between `Path<T, P, { depth: 5 }>` and `string` is necessary for TypeScript to successfully narrow down the type of `P` based on the user-provided `path` input.
Without the intersection, the `path` would just be of type `Path<T, P, { depth: 5 }>` and `PathValueEntry` would be a union of all possible return types.
By using the intersection, TypeScript is forced to apply the `Path` constraints and infer the type from the provided user input.

<br>
Expand All @@ -223,21 +231,44 @@ By using the intersection, TypeScript is forced to apply the `Path` constraints

### ❗ Why are my paths truncated in a object with index signature?

See this [issue](https://github.com/clickbar/dot-diver/issues/2).
Paths get truncated, if they are unioned with a string. E.g. `keyof T | string`.\
This should only happen in rare cases for objects looking like this:

```typescript
type TestType = {
a: string
b: string
[key: string]: string
}
```
If your object has nested properties, for example looking like this:
```typescript
type TestType = {
a: string
b: {
c: string
}
[key: string]: string
}
```
You will get autocompletion again, as soon as you typed the path to the nested object, e.g. `b.`.
<br>
### ❗ Why are my paths truncated inside an array?
Your paths are not truncated. Typescript will still validate them.
Your paths are not truncated. TypeScript will still validate them.
Some IDEs have problems with displaying `children.${number}` paths.
If you can, define the array as an tuple. This will include all paths in the auto completion.
If you can, define the array as an tuple. This will include all paths in the autocompletion.
<br>
### ❗ I get the error "Type instantiation is excessively deep and possibly infinite.ts(2589)"
This happens if typescript reaches its maximum depth limit. This library should prevent this, but it can still happen if a object has a lot of cyclic dependencies.\
This happens if TypeScript reaches its maximum depth limit. This library should prevent this, but it can still happen if a object has a lot of cyclic dependencies.\
For example:
```typescript
Expand All @@ -248,12 +279,12 @@ type TestType = {
d: {
e: TestType
}
f: TestType2
f: TestType
}
```
You can try to decrease the depth limit of the auto completion by reimplementing the `getByPath` and `setByPath` functions.
See [this section](#%EF%B8%8F-customizing-the-depth-limit) for customizing the depth limit.
You can try to decrease the lookahead depth of the autocompletion by reimplementing the `getByPath` and `setByPath` functions.
See [this section](#%EF%B8%8F-customizing-the-depth-lookahead-limit).
<br>
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ These versions of dot-diver are currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
| 2.x | :white_check_mark: |

## Reporting a Vulnerability

Expand Down
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-env node */
import typescriptConfig from '@clickbar/eslint-config-typescript'

export default [
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
...typescriptConfig(),
]
28 changes: 15 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"name": "@clickbar/dot-diver",
"version": "1.0.6",
"version": "2.0.0",
"description": "Types and utilities to access object properties by dot notation.",
"packageManager": "[email protected]",
"types": "dist/index.d.ts",
"exports": {
".": {
Expand All @@ -19,9 +18,10 @@
"scripts": {
"build": "vite build",
"test": "vitest run --typecheck",
"lint": "eslint ./ --ext=js,ts",
"lint": "eslint ./",
"prettier": "prettier . --cache",
"fix": "pnpm lint --fix && pnpm prettier --write"
"fix": "pnpm lint --fix && pnpm prettier --write",
"perf": "tsc --noEmit --extendedDiagnostics --p ./performance/tsconfig.json"
},
"keywords": [
"ts",
Expand All @@ -45,13 +45,15 @@
"Tobias Kröll <[email protected]> (https://github.com/saibotk)"
],
"devDependencies": {
"@clickbar/eslint-config-typescript": "^10.2.0",
"@types/node": "^20.12.12",
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-dts": "^3.9.1",
"vitest": "^1.6.0"
}
"@clickbar/eslint-config-typescript": "^11.0.4",
"@types/node": "^22.4.0",
"@vue/reactivity": "^3.4.38",
"eslint": "^9.9.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^5.4.1",
"vite-plugin-dts": "^4.0.3",
"vitest": "^2.0.5"
},
"packageManager": "[email protected]+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
}
Loading

0 comments on commit 00dc0a8

Please sign in to comment.