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
11 changes: 11 additions & 0 deletions .changeset/add-path-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'flattify': minor
---

Add the `flattify/path` entry to read and write nested values by path, with autocompleted paths and typed values:

- `get(obj, 'items.0.sku')`: the result is typed from `obj` (`string | undefined`)
- `set(obj, 'db.port', 6543)`: the value is checked against the path, and missing objects and arrays are created
- `has`, `del`, `paths` (the paths `flatten` produces), `parsePath`, `stringifyPath` and `escapeKey`
- `Path<T>`, `Get<T, P>` and `SetValue<T, P>` types
- Every helper takes the `delimiter`, `notation` and `escape` options, and is safe against prototype pollution
5 changes: 5 additions & 0 deletions docs/file-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ project stays on Miniflare 4 because Miniflare 5 is still an alpha with a differ
### test/typescript-5.0
Compiles a consumer file against the built `.d.ts` with TypeScript 5.0 (`pnpm run test:ts-legacy`),
the oldest version supported: the public types rely on `const` type parameters, added in 5.0.

### path/package.json
Makes `import ... from 'flattify/path'` resolve with `moduleResolution: "node"` (node10) and older
bundlers, which ignore the `exports` field of `package.json`. They look for a `path` folder in the
package and follow its `main`, `module` and `types` fields to `dist/`. Checked by `attw` in `lint:package`.
5 changes: 4 additions & 1 deletion jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"name": "@nandomb/flattify",
"version": "1.0.2",
"license": "MIT",
"exports": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./path": "./src/path/index.ts"
},
"publish": {
"include": [
"src/**/*.ts",
Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,21 @@
"default": "./dist/index.cjs"
}
},
"./path": {
"import": {
"types": "./dist/path.d.ts",
"default": "./dist/path.js"
},
"require": {
"types": "./dist/path.d.cts",
"default": "./dist/path.cjs"
}
},
"./package.json": "./package.json"
},
"files": [
"dist"
"dist",
"path"
],
"scripts": {
"dev": "vitest --project node",
Expand Down
6 changes: 6 additions & 0 deletions path/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"private": true,
"main": "../dist/path.cjs",
"module": "../dist/path.js",
"types": "../dist/path.d.cts"
}
14 changes: 7 additions & 7 deletions src/flatten/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export interface FlattenOptions {
}

/** Nesting levels TypeScript expands before typing deeper paths as `unknown`. */
type TypeDepthLimit = 10;
export type TypeDepthLimit = 10;

interface Context {
export interface Context {
delimiter: string;
notation: Notation;
maxDepth: number | undefined;
Expand All @@ -34,10 +34,10 @@ interface Context {
escape: boolean;
}

type Kind<V, C extends Context> = V extends Leaf ? 'leaf' : V extends readonly unknown[] ? (C['safe'] extends true ? 'leaf' : 'array') : V extends object ? ([keyof V] extends [never] ? 'unknown' : 'object') : 'leaf';
export type Kind<V, C extends Context> = V extends Leaf ? 'leaf' : V extends readonly unknown[] ? (C['safe'] extends true ? 'leaf' : 'array') : V extends object ? ([keyof V] extends [never] ? 'unknown' : 'object') : 'leaf';

type LeavesOf<V, C extends Context> = V extends unknown ? (Kind<V, C> extends 'leaf' ? V : never) : never;
type ContainersOf<V, C extends Context> = V extends unknown ? (Kind<V, C> extends 'leaf' ? never : V) : never;
export type ContainersOf<V, C extends Context> = V extends unknown ? (Kind<V, C> extends 'leaf' ? never : V) : never;

/** Mirrors `escapeKey`: see `src/shared/path.ts`. `X` is `'['` in bracket notation. */
type EscapeKey<K extends string, D extends string, X extends string> = NeedsEscape<K, D, X> extends true
Expand All @@ -50,7 +50,7 @@ type NeedsEscape<K extends string, D extends string, X extends string> = K exten
type Overlaps<D extends string> = Prefixes<D> extends infer P ? (P extends string ? (`${P}${D}` extends `${D}${string}` ? P : never) : never) : never;
type Prefixes<D extends string, Acc extends string = ''> = D extends `${infer Char}${infer Rest}` ? (Rest extends '' ? never : `${Acc}${Char}` | Prefixes<Rest, `${Acc}${Char}`>) : never;
/** Mirrors `pathFormat().join`: appends key `K` to path `P` (the top level is at depth `[]`). */
type Join<P extends string, K extends string | number, IsIndex extends boolean, D extends unknown[], C extends Context> = C['notation'] extends 'pointer'
export type Join<P extends string, K extends string | number, IsIndex extends boolean, D extends unknown[], C extends Context> = C['notation'] extends 'pointer'
? `${P}/${K extends string ? ReplaceAll<ReplaceAll<K, '~', '~0'>, '/', '~1'> : K}`
: C['notation'] extends 'bracket'
? IsIndex extends true
Expand All @@ -60,7 +60,7 @@ type Join<P extends string, K extends string | number, IsIndex extends boolean,
type JoinKey<P extends string, K extends string, D extends unknown[], C extends Context> = D extends [] ? K : `${P}${C['delimiter']}${K}`;

/** Any path below `K`, for values whose shape is unknown. */
type Below<K extends string, C extends Context> = C['notation'] extends 'pointer' ? `${K}/${string}` : C['notation'] extends 'bracket' ? `${K}${C['delimiter']}${string}` | `${K}[${string}` : `${K}${C['delimiter']}${string}`;
export type Below<K extends string, C extends Context> = C['notation'] extends 'pointer' ? `${K}/${string}` : C['notation'] extends 'bracket' ? `${K}${C['delimiter']}${string}` | `${K}[${string}` : `${K}${C['delimiter']}${string}`;

/** `?` is skipped for template keys (`` `tags.${number}.name` ``): they are index signatures, where it would only add `undefined`. */
type Field<K extends string, V, Optional extends boolean> = [V] extends [never] ? {} : Optional extends true ? ({} extends Record<K, 1> ? { [_ in K]: V } : { [_ in K]?: V }) : { [_ in K]: V };
Expand Down Expand Up @@ -112,7 +112,7 @@ type ObjectEntries<T, P extends string, IsArray extends boolean, D extends unkno
/** Index signatures (`[key: string]: V`) are left required: `?` would add `undefined` to their values. */
type IsOptional<T, K extends keyof T> = string extends K ? false : number extends K ? false : {} extends Pick<T, K> ? true : false;

type TupleEntries<T extends readonly unknown[]> = { [K in keyof T as K extends `${number}` ? K : never]: T[K] };
export type TupleEntries<T extends readonly unknown[]> = { [K in keyof T as K extends `${number}` ? K : never]: T[K] };

type FlatContainer<T, P extends string, D extends unknown[], C extends Context> = T extends readonly unknown[]
? number extends T['length']
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@

export * from './flatten/index.ts';
export * from './unflatten/index.ts';
export type { Notation } from './shared/notation.ts';
export type { Prettify } from './shared/types.ts';
18 changes: 18 additions & 0 deletions src/path/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Read and write nested values by path, with autocompleted paths and typed values.
*
* @example
* ```ts
* import { get, set } from 'flattify/path';
*
* const config = { db: { host: 'localhost', port: 5432 } };
* get(config, 'db.port'); // 5432, typed number
* set(config, 'db.host', 'db.internal');
* ```
*
* @module
*/

export { del, escapeKey, get, has, parsePath, paths, set, stringifyPath } from './path.ts';
export type { Get, Path, PathOptions, SetOptions, SetValue } from './types.ts';
export type { Notation } from '../shared/notation.ts';
Loading
Loading