Skip to content

Commit 5dbd6c8

Browse files
committed
[New] add types
1 parent 0a50205 commit 5dbd6c8

File tree

13 files changed

+113
-21
lines changed

13 files changed

+113
-21
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"rules": {
77
"consistent-return": 1,
8+
"max-len": 0,
89
"max-lines-per-function": 0,
910
"max-params": 1,
1011
"max-statements-per-line": [2, { "max": 2 }],

example/value_cmp.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ var stringify = require('../');
44

55
var obj = { d: 6, c: 5, b: [{ z: 3, y: 2, x: 1 }, 9], a: 10 };
66

7-
var s = stringify(obj, function (a, b) {
7+
var s = stringify(obj, /** @type {import('..').Comparator} */ function (a, b) {
8+
// @ts-expect-error implicit coercion here is fine
89
return a.value < b.value ? 1 : -1;
910
});
1011

index.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
declare namespace stableStringify {
2+
type Key = string | number;
3+
4+
type NonArrayNode = Record<Key, unknown>;
5+
type Node = unknown[] | NonArrayNode;
6+
7+
type Getter = { get(key: Key): unknown };
8+
9+
type Comparator = (a: { key: string, value: unknown }, b: { key: string, value: unknown }, getter: Getter) => number;
10+
11+
type StableStringifyOptions = {
12+
cmp?: Comparator;
13+
cycles?: boolean;
14+
replacer?: (this: Node, key: Key, value: unknown) => unknown;
15+
space?: string | number;
16+
};
17+
}
18+
19+
declare function stableStringify(
20+
obj: object,
21+
options?: (stableStringify.Comparator & stableStringify.StableStringifyOptions) | stableStringify.StableStringifyOptions,
22+
): string | undefined;
23+
24+
export = stableStringify;

index.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
/** @type {typeof JSON.stringify} */
34
var jsonStringify = (typeof JSON !== 'undefined' ? JSON : require('jsonify')).stringify;
45

56
var isArray = require('isarray');
@@ -12,6 +13,7 @@ var $indexOf = callBound('Array.prototype.indexOf');
1213
var $splice = callBound('Array.prototype.splice');
1314
var $sort = callBound('Array.prototype.sort');
1415

16+
/** @type {(n: number, char: string) => string} */
1517
var strRepeat = function repeat(n, char) {
1618
var str = '';
1719
for (var i = 0; i < n; i += 1) {
@@ -20,35 +22,47 @@ var strRepeat = function repeat(n, char) {
2022
return str;
2123
};
2224

23-
var defaultReplacer = function (parent, key, value) { return value; };
25+
/** @type {(parent: import('.').Node, key: import('.').Key, value: unknown) => unknown} */
26+
var defaultReplacer = function (_parent, _key, value) { return value; };
2427

28+
/** @type {import('.')} */
2529
module.exports = function stableStringify(obj) {
30+
/** @type {Parameters<import('.')>[1]} */
2631
var opts = arguments.length > 1 ? arguments[1] : void undefined;
2732
var space = (opts && opts.space) || '';
2833
if (typeof space === 'number') { space = strRepeat(space, ' '); }
2934
var cycles = !!opts && typeof opts.cycles === 'boolean' && opts.cycles;
35+
/** @type {undefined | typeof defaultReplacer} */
3036
var replacer = opts && opts.replacer ? callBind(opts.replacer) : defaultReplacer;
3137

3238
var cmpOpt = typeof opts === 'function' ? opts : opts && opts.cmp;
39+
/** @type {undefined | (<T extends import('.').NonArrayNode>(node: T) => (a: Exclude<keyof T, symbol | number>, b: Exclude<keyof T, symbol | number>) => number)} */
3340
var cmp = cmpOpt && function (node) {
34-
var get = cmpOpt.length > 2 && function get(k) { return node[k]; };
41+
// eslint-disable-next-line no-extra-parens
42+
var get = /** @type {NonNullable<typeof cmpOpt>} */ (cmpOpt).length > 2
43+
&& /** @type {import('.').Getter['get']} */ function get(k) { return node[k]; };
3544
return function (a, b) {
36-
return cmpOpt(
45+
// eslint-disable-next-line no-extra-parens
46+
return /** @type {NonNullable<typeof cmpOpt>} */ (cmpOpt)(
3747
{ key: a, value: node[a] },
3848
{ key: b, value: node[b] },
39-
get ? { __proto__: null, get: get } : void undefined
49+
// @ts-expect-error TS doesn't understand the optimization used here
50+
get ? /** @type {import('.').Getter} */ { __proto__: null, get: get } : void undefined
4051
);
4152
};
4253
};
4354

55+
/** @type {import('.').Node[]} */
4456
var seen = [];
45-
return (
57+
return (/** @type {(parent: import('.').Node, key: string | number, node: unknown, level: number) => string | undefined} */
4658
function stringify(parent, key, node, level) {
4759
var indent = space ? '\n' + strRepeat(level, space) : '';
4860
var colonSeparator = space ? ': ' : ':';
4961

50-
if (node && node.toJSON && typeof node.toJSON === 'function') {
51-
node = node.toJSON();
62+
// eslint-disable-next-line no-extra-parens
63+
if (node && /** @type {{ toJSON?: unknown }} */ (node).toJSON && typeof /** @type {{ toJSON?: unknown }} */ (node).toJSON === 'function') {
64+
// eslint-disable-next-line no-extra-parens
65+
node = /** @type {{ toJSON: Function }} */ (node).toJSON();
5266
}
5367

5468
node = replacer(parent, key, node);
@@ -72,14 +86,17 @@ module.exports = function stableStringify(obj) {
7286
if (cycles) { return jsonStringify('__cycle__'); }
7387
throw new TypeError('Converting circular structure to JSON');
7488
} else {
75-
seen[seen.length] = node;
89+
seen[seen.length] = /** @type {import('.').NonArrayNode} */ (node);
7690
}
7791

78-
var keys = $sort(objectKeys(node), cmp && cmp(node));
92+
/** @type {import('.').Key[]} */
93+
// eslint-disable-next-line no-extra-parens
94+
var keys = $sort(objectKeys(node), cmp && cmp(/** @type {import('.').NonArrayNode} */ (node)));
7995
var out = [];
8096
for (var i = 0; i < keys.length; i++) {
8197
var key = keys[i];
82-
var value = stringify(node, key, node[key], level + 1);
98+
// eslint-disable-next-line no-extra-parens
99+
var value = stringify(/** @type {import('.').Node} */ (node), key, /** @type {import('.').NonArrayNode} */ (node)[key], level + 1);
83100

84101
if (!value) { continue; }
85102

@@ -91,7 +108,6 @@ module.exports = function stableStringify(obj) {
91108
}
92109
$splice(seen, $indexOf(seen, node), 1);
93110
return '{' + $join(out, ',') + indent + '}';
94-
95111
}({ '': obj }, '', obj, 0)
96112
);
97113
};

package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"prepublishOnly": "safe-publish-latest",
99
"prepublish": "not-in-publish || npm run prepublishOnly",
1010
"lint": "eslint --ext=js,mjs .",
11+
"postlint": "tsc && attw -P",
1112
"pretest": "npm run lint",
1213
"tests-only": "tape 'test/**/*.js'",
1314
"test": "npm run tests-only",
@@ -48,14 +49,21 @@
4849
"object-keys": "^1.1.1"
4950
},
5051
"devDependencies": {
52+
"@arethetypeswrong/cli": "^0.17.1",
5153
"@ljharb/eslint-config": "^21.1.1",
54+
"@ljharb/tsconfig": "^0.2.2",
55+
"@types/call-bind": "^1.0.5",
56+
"@types/isarray": "^2.0.3",
57+
"@types/object-keys": "^1.0.3",
58+
"@types/tape": "^5.7.0",
5259
"auto-changelog": "^2.5.0",
5360
"encoding": "^0.1.13",
5461
"eslint": "=8.8.0",
5562
"in-publish": "^2.0.1",
5663
"npmignore": "^0.3.1",
5764
"safe-publish-latest": "^2.0.0",
58-
"tape": "^5.9.0"
65+
"tape": "^5.9.0",
66+
"typescript": "next"
5967
},
6068
"engines": {
6169
"node": ">= 0.4"
@@ -73,7 +81,8 @@
7381
},
7482
"publishConfig": {
7583
"ignore": [
76-
".github/workflows"
84+
".github/workflows",
85+
"types"
7786
]
7887
}
7988
}

test/cmp.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ test('custom comparison function', function (t) {
1515
test('custom comparison function with get', function (t) {
1616
t.plan(2);
1717

18-
stringify({ a: 1, b: 2 }, function (a, b) { // eslint-disable-line no-unused-vars
18+
stringify({ a: 1, b: 2 }, /** @type {import('..').Comparator} */ function (_a, _b) { // eslint-disable-line no-unused-vars
1919
t.equal(arguments[2], undefined, 'comparator options not passed when not explicitly requested');
20+
return NaN;
2021
});
2122

2223
var obj = { c: 8, b: [{ z: 7, y: 6, x: 4, v: 2, '!v': 3 }, 7], a: 3 };
2324
var s = stringify(obj, function (a, b, options) {
2425
var get = options.get;
26+
// @ts-expect-error implicit coercion here is fine
2527
var v1 = (get('!' + a.key) || 0) + a.value;
28+
// @ts-expect-error implicit coercion here is fine
2629
var v2 = (get('!' + b.key) || 0) + b.value;
2730
return v1 - v2;
2831
});

test/nested.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ test('nested', function (t) {
1111

1212
test('cyclic (default)', function (t) {
1313
t.plan(1);
14-
var one = { a: 1 };
14+
var one = { a: 1, two: {} };
1515
var two = { a: 2, one: one };
1616
one.two = two;
1717
try {
1818
stringify(one);
1919
} catch (ex) {
20-
t.equal(ex.toString(), 'TypeError: Converting circular structure to JSON');
20+
if (ex == null) { // eslint-disable-line eqeqeq
21+
t.fail('nullish exception');
22+
} else {
23+
t.equal(ex.toString(), 'TypeError: Converting circular structure to JSON');
24+
}
2125
}
2226
});
2327

2428
test('cyclic (specifically allowed)', function (t) {
2529
t.plan(1);
26-
var one = { a: 1 };
30+
var one = { a: 1, two: {} };
2731
var two = { a: 2, one: one };
2832
one.two = two;
2933
t.equal(stringify(one, { cycles: true }), '{"a":1,"two":{"a":2,"one":"__cycle__"}}');

test/replacer.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ test('replace numbers', function (t) {
1616
t.plan(1);
1717

1818
var obj = { a: 1, b: 2, c: false };
19-
var replacer = function (key, value) {
19+
/** @type {import('..').StableStringifyOptions['replacer']} */
20+
var replacer = function (_key, value) {
2021
if (value === 1) { return 'one'; }
2122
if (value === 2) { return 'two'; }
2223
return value;
@@ -29,6 +30,7 @@ test('replace with object', function (t) {
2930
t.plan(1);
3031

3132
var obj = { a: 1, b: 2, c: false };
33+
/** @type {import('..').StableStringifyOptions['replacer']} */
3234
var replacer = function (key, value) {
3335
if (key === 'b') { return { d: 1 }; }
3436
if (value === 1) { return 'one'; }
@@ -42,7 +44,8 @@ test('replace with undefined', function (t) {
4244
t.plan(1);
4345

4446
var obj = { a: 1, b: 2, c: false };
45-
var replacer = function (key, value) {
47+
/** @type {import('..').StableStringifyOptions['replacer']} */
48+
var replacer = function (_key, value) {
4649
if (value === false) { return; }
4750
return value;
4851
};
@@ -54,6 +57,7 @@ test('replace with array', function (t) {
5457
t.plan(1);
5558

5659
var obj = { a: 1, b: 2, c: false };
60+
/** @type {import('..').StableStringifyOptions['replacer']} */
5761
var replacer = function (key, value) {
5862
if (key === 'b') { return ['one', 'two']; }
5963
return value;
@@ -66,7 +70,8 @@ test('replace array item', function (t) {
6670
t.plan(1);
6771

6872
var obj = { a: 1, b: 2, c: [1, 2] };
69-
var replacer = function (key, value) {
73+
/** @type {import('..').StableStringifyOptions['replacer']} */
74+
var replacer = function (_key, value) {
7075
if (value === 1) { return 'one'; }
7176
if (value === 2) { return 'two'; }
7277
return value;

test/space.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ test('space parameter (same as native)', function (t) {
7474
test('space parameter, on a cmp function', function (t) {
7575
t.plan(3);
7676
var obj = { one: 1, two: 2 };
77+
/** @type {import('..').Comparator & import('../').StableStringifyOptions} */
7778
var cmp = function (a, b) {
7879
return a < b ? 1 : -1;
7980
};

tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "@ljharb/tsconfig",
3+
"compilerOptions": {
4+
"maxNodeModuleJsDepth": 0,
5+
"target": "es2021",
6+
},
7+
"exclude": [
8+
"coverage",
9+
],
10+
}

0 commit comments

Comments
 (0)