/
index.ts
163 lines (140 loc) · 4.34 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Note that this is in a subfolder within src to mimick that when compiled it will be put in dist/{cjs,esm} which is
// equally a subfolder within dist. This is to ensure that the import path to the package.json is correct.
import nodePreGyp from '@discordjs/node-pre-gyp';
import { join, resolve } from 'node:path';
declare namespace Internals {
export function getPromiseDetails(thing: any): number[];
export function getProxyDetails(thing: any): number[];
}
const bindingPath = nodePreGyp.find(resolve(join(__dirname, '..', '..', './package.json')));
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const { getPromiseDetails, getProxyDetails } = require(bindingPath) as typeof Internals;
/**
* The class for deep checking Types
*/
export class Type {
/**
* The value to generate a deep Type of
*/
public readonly value: unknown;
/**
* The shallow type of this
*/
public is: string;
/**
* The parent of this Type
*/
private readonly parent: Type | null;
/**
* The child keys of this Type
*/
private readonly childKeys = new Map<string, Type>();
/**
* The child values of this Type
*/
private readonly childValues = new Map<string, Type>();
/**
* @param value The value to generate a deep Type of
* @param parent The parent value used in recursion
*/
public constructor(value: unknown, parent: Type | null = null) {
this.value = value;
this.is = Type.resolve(value);
this.parent = parent;
}
/**
* The type string for the children of this Type
*/
private get childTypes(): string {
if (!this.childValues.size) return '';
return `<${(this.childKeys.size ? `${Type.list(this.childKeys)}, ` : '') + Type.list(this.childValues)}>`;
}
/**
* The full type string generated.
*/
public toString(): string {
this.check();
return `${this.is}${this.childTypes}`;
}
/**
* Walks the linked list backwards, for checking circulars.
*/
private *parents(): IterableIterator<Type> {
// eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
let current: Type | null = this;
while ((current = current.parent)) yield current;
}
/**
* Checks if the value of this Type is a circular reference to any parent.
*/
private isCircular(): boolean {
for (const parent of this.parents()) if (parent.value === this.value) return true;
return false;
}
/**
* The subtype to create based on this.value's sub value.
* @param value The sub value
*/
private addValue(value: unknown): void {
const child = new Type(value, this);
this.childValues.set(child.is, child);
}
/**
* The subtype to create based on this.value's entries.
* @param entry The entry
*/
private addEntry([key, value]: [string, unknown]): void {
const child = new Type(key, this);
this.childKeys.set(child.is, child);
this.addValue(value);
}
/**
* Get the deep type name that defines the input.
*/
private check(): void {
if (Object.isFrozen(this)) return;
const promise = getPromiseDetails(this.value);
const proxy = getProxyDetails(this.value);
if (typeof this.value === 'object' && this.isCircular()) {
this.is = `[Circular:${this.is}]`;
} else if (promise && promise[0]) {
this.addValue(promise[1]);
} else if (proxy && proxy[0]) {
this.is = 'Proxy';
this.addValue(proxy[0]);
} else if (this.value instanceof Map) {
for (const entry of this.value) this.addEntry(entry);
} else if (Array.isArray(this.value) || this.value instanceof Set) {
for (const value of this.value) this.addValue(value);
} else if (this.is === 'Object') {
this.is = 'Record';
for (const entry of Object.entries(this.value as Record<PropertyKey, unknown>)) this.addEntry(entry);
}
Object.freeze(this);
}
/**
* Resolves the type name that defines the input.
* @param value The value to get the type name of
*/
public static resolve(value: any): string {
const type = typeof value;
switch (type) {
case 'object':
return value === null ? 'null' : value.constructor ? value.constructor.name : 'Object';
case 'function':
return `${value.constructor.name}(${value.length}-arity)`;
case 'undefined':
return 'void';
default:
return type;
}
}
/**
* Joins the list of child types.
* @param values The values to list
*/
private static list(values: Map<string, Type>): string {
return [...values.values()].sort().join(' | ');
}
}
export default Type;