Skip to content

Commit ee8a4ec

Browse files
authored
feat: Improve type safety of custom transform functions (#10)
1 parent d3c1428 commit ee8a4ec

File tree

5 files changed

+37
-36
lines changed

5 files changed

+37
-36
lines changed

src/map.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ interface KeyValuePair<K, V> {
1313
/**
1414
* A Map implementation that supports deep equality for object keys.
1515
*/
16-
export class DeepMap<K, V> extends Map<K, V> implements Comparable<DeepMap<K, V>> {
17-
private readonly normalizer: Normalizer<K, V>;
18-
private readonly map: Map<Normalized<K>, KeyValuePair<K, V>>;
16+
export class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Comparable<DeepMap<K, V, TxK, TxV>> {
17+
private readonly normalizer: Normalizer<K, V, TxK, TxV>;
18+
private readonly map: Map<Normalized<TxK>, KeyValuePair<K, V>>;
1919

2020
// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
21-
constructor(entries?: readonly (readonly [K, V])[] | null, options: Options<K, V> = {}) {
21+
constructor(entries?: readonly (readonly [K, V])[] | null, options: Options<K, V, TxK, TxV> = {}) {
2222
super();
2323
this.normalizer = new Normalizer(options);
2424
const transformedEntries = entries
@@ -142,11 +142,11 @@ export class DeepMap<K, V> extends Map<K, V> implements Comparable<DeepMap<K, V>
142142

143143
// PRIVATE METHODS FOLLOW...
144144

145-
private normalizeKey(input: K): Normalized<K> {
145+
private normalizeKey(input: K): Normalized<TxK> {
146146
return this.normalizer.normalizeKey(input);
147147
}
148148

149-
private normalizeValue(input: V): Normalized<V> {
149+
private normalizeValue(input: V): Normalized<TxV> {
150150
return this.normalizer.normalizeValue(input);
151151
}
152152
}

src/normalizer.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ type HashedObject = string;
1111
/**
1212
* Type for normalized input.
1313
*/
14-
export type Normalized<T> = HashedObject | T | ReturnType<TransformFunction<T>>;
14+
export type Normalized<T> = HashedObject | T;
1515

1616
/**
1717
* Class that normalizes object types to strings via hashing
1818
*/
19-
export class Normalizer<K, V> {
19+
export class Normalizer<K, V, TxK, TxV> {
2020
private readonly objectHashOptions: ObjectHashOptions;
21-
private readonly keyTransformer: TransformFunction<K>;
22-
private readonly valueTransformer: TransformFunction<V>;
21+
private readonly keyTransformer: TransformFunction<K, TxK>;
22+
private readonly valueTransformer: TransformFunction<V, TxV>;
2323

24-
constructor(options: Options<K, V> = {}) {
24+
constructor(options: Options<K, V, TxK, TxV> = {}) {
2525
const { transformer, mapValueTransformer, useToJsonTransform, ...objectHashOptions } =
2626
getOptionsWithDefaults(options);
2727
this.keyTransformer = useToJsonTransform ? Transformers.jsonSerializeDeserialize : transformer;
@@ -34,7 +34,7 @@ export class Normalizer<K, V> {
3434
* @param input the input to normalize
3535
* @returns the normalized result
3636
*/
37-
normalizeKey(input: K): Normalized<K> {
37+
normalizeKey(input: K): Normalized<TxK> {
3838
return this.normalizeHelper(this.keyTransformer(input));
3939
}
4040

@@ -43,7 +43,7 @@ export class Normalizer<K, V> {
4343
* @param input the input to normalize
4444
* @returns the normalized result
4545
*/
46-
normalizeValue(input: V): Normalized<V> {
46+
normalizeValue(input: V): Normalized<TxV> {
4747
return this.normalizeHelper(this.valueTransformer(input));
4848
}
4949

src/options.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import { Require } from './utils';
66
/**
77
* Library options
88
*/
9-
interface DeepEqualityDataStructuresOptions<K, V> {
9+
interface DeepEqualityDataStructuresOptions<K, V, TxK, TxV> {
1010
/**
1111
* A function that transforms Map keys or Set values prior to normalization.
1212
*
1313
* NOTE: The caller is responsible for not mutating object inputs.
1414
*/
15-
transformer?: TransformFunction<K>;
15+
transformer?: TransformFunction<K, TxK>;
1616

1717
/**
1818
* A function that transforms Map values prior to normalization.
1919
*
2020
* NOTE: The caller is responsible for not mutating object inputs.
2121
*/
22-
mapValueTransformer?: TransformFunction<V>;
22+
mapValueTransformer?: TransformFunction<V, TxV>;
2323

2424
/**
2525
* If true, objects will be JSON-serialized/deserialized into "plain" objects prior to hashing.
@@ -29,14 +29,14 @@ interface DeepEqualityDataStructuresOptions<K, V> {
2929
useToJsonTransform?: boolean;
3030
}
3131

32-
export type Options<K, V> = ObjectHashOptions & DeepEqualityDataStructuresOptions<K, V>;
32+
export type Options<K, V, TxK, TxV> = ObjectHashOptions & DeepEqualityDataStructuresOptions<K, V, TxK, TxV>;
3333

3434
/**
3535
* Given the specified options, resolve default values as appropriate.
3636
*/
37-
export function getOptionsWithDefaults<K, V>(
38-
options: Options<K, V>
39-
): Require<Options<K, V>, keyof DeepEqualityDataStructuresOptions<K, V>> {
37+
export function getOptionsWithDefaults<K, V, TxK, TxV>(
38+
options: Options<K, V, TxK, TxV>
39+
): Require<Options<K, V, TxK, TxV>, keyof DeepEqualityDataStructuresOptions<K, V, TxK, TxV>> {
4040
return {
4141
// Default options
4242
algorithm: 'md5' as const, // not a cryptographic usage, who cares

src/set.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { Options } from './options';
55
/**
66
* A Set implementation that supports deep equality for values.
77
*/
8-
export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
8+
export class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V, TxV>> {
99
// Just piggy-back on a DeepMap that uses null values
10-
private readonly map: DeepMap<T, null>;
10+
private readonly map: DeepMap<V, null, TxV, null>;
1111

1212
// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
13-
constructor(values?: readonly T[] | null, options?: Options<T, null>) {
13+
constructor(values?: readonly V[] | null, options?: Options<V, null, TxV, null>) {
1414
super();
1515
const transformedEntries = values ? values.map((el) => [el, null] as const) : null;
1616
this.map = new DeepMap(transformedEntries, options);
@@ -26,22 +26,22 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
2626
/**
2727
* Returns true if the given value is present in the set.
2828
*/
29-
override has(key: T): boolean {
30-
return this.map.has(key);
29+
override has(val: V): boolean {
30+
return this.map.has(val);
3131
}
3232

3333
/**
3434
* Store the given value.
3535
*/
36-
override add(val: T): this {
36+
override add(val: V): this {
3737
this.map.set(val, null);
3838
return this;
3939
}
4040

4141
/**
4242
* Deletes the specified value.
4343
*/
44-
override delete(val: T): boolean {
44+
override delete(val: V): boolean {
4545
return this.map.delete(val);
4646
}
4747

@@ -55,7 +55,7 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
5555
/**
5656
* Standard forEach function.
5757
*/
58-
override forEach(callbackfn: (val: T, val2: T, set: Set<T>) => void): void {
58+
override forEach(callbackfn: (val: V, val2: V, set: Set<V>) => void): void {
5959
this.map.forEach((_mapVal, mapKey, _map) => {
6060
callbackfn(mapKey, mapKey, this);
6161
});
@@ -66,7 +66,7 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
6666
*
6767
* @yields the next value in the set
6868
*/
69-
override *[Symbol.iterator](): IterableIterator<T> {
69+
override *[Symbol.iterator](): IterableIterator<V> {
7070
for (const [key, _val] of this.map[Symbol.iterator]()) {
7171
yield key;
7272
}
@@ -77,7 +77,7 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
7777
*
7878
* @yields the next value-value pair in the set
7979
*/
80-
override *entries(): IterableIterator<[T, T]> {
80+
override *entries(): IterableIterator<[V, V]> {
8181
for (const val of this[Symbol.iterator]()) {
8282
yield [val, val];
8383
}
@@ -88,7 +88,7 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
8888
*
8989
* @yields the next key in the map
9090
*/
91-
override *keys(): IterableIterator<T> {
91+
override *keys(): IterableIterator<V> {
9292
for (const val of this[Symbol.iterator]()) {
9393
yield val;
9494
}
@@ -99,7 +99,7 @@ export class DeepSet<T> extends Set<T> implements Comparable<DeepSet<T>> {
9999
*
100100
* @yields the next value in the map
101101
*/
102-
override *values(): IterableIterator<T> {
102+
override *values(): IterableIterator<V> {
103103
yield* this.keys();
104104
}
105105

src/transformers.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
export type TransformFunction<T> = (input: T) => unknown;
1+
export type TransformFunction<T, R> = (input: T) => R;
22

33
export class Transformers {
4-
static identity<T>(input: T): T {
5-
return input;
4+
static identity<T, R = T>(input: T): R {
5+
// Just make the types happy :)
6+
return input as unknown as R;
67
}
78

8-
static jsonSerializeDeserialize<T>(obj: T): T {
9+
static jsonSerializeDeserialize<T, R = T>(obj: T): R {
910
return JSON.parse(JSON.stringify(obj));
1011
}
1112
}

0 commit comments

Comments
 (0)