@murky-web/typebuddy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # TypeBuddy
2
+
3
+ TypeBuddy is a utility library for type guards, parsing, and small TypeScript-first helper types. It focuses on two things:
4
+
5
+ - predictable runtime helpers for common JavaScript values
6
+ - a small type system around `Optional`, `Maybe`, and `Nullable`
7
+
8
+ ## Features
9
+
10
+ - runtime guards for common JavaScript values
11
+ - parsing helpers for strings, numbers, arrays, and URLs
12
+ - utility types like `Optional<T>`, `Maybe<T>`, `Nullable<T>`
13
+ - type-system guards like `isOptional`, `isMaybe`, `isNullable`
14
+ - global type opt-in through `@murky-web/typebuddy/globals`
15
+ - custom `oxlint` rules through `@murky-web/typebuddy/oxlint`
16
+ - tree-shake-friendly ESM output for frontend and backend bundles
17
+
18
+ ## Installation
19
+
20
+ From npm:
21
+
22
+ ```bash
23
+ bun add @murky-web/typebuddy
24
+ ```
25
+
26
+ From JSR:
27
+
28
+ ```bash
29
+ npx jsr add @murky-web/typebuddy
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Here's an example of how to use TypeBuddy in your TypeScript project:
35
+
36
+ ```typescript
37
+ import { isString, parseNumber, isUuidString, isEmptyObject } from "@murky-web/typebuddy";
38
+
39
+ const value: unknown = "123";
40
+
41
+ if (isString(value)) {
42
+ console.log(`The value is a string: ${value}`);
43
+ }
44
+
45
+ const numberValue = parseNumber(value);
46
+ console.log(`Parsed number: ${numberValue}`);
47
+
48
+ const uuid = "550e8400-e29b-41d4-a716-446655440000";
49
+ if (isUuidString(uuid)) {
50
+ console.log(`The value is a valid UUID: ${uuid}`);
51
+ }
52
+
53
+ const obj = {};
54
+ if (isEmptyObject(obj)) {
55
+ console.log("The object is empty");
56
+ }
57
+ ```
58
+
59
+ ### Type-system guards
60
+
61
+ ```typescript
62
+ import { isArray, isNullable, isOptional } from "@murky-web/typebuddy";
63
+ import type { Nullable, Optional } from "@murky-web/typebuddy";
64
+
65
+ const title: Optional<string> = "ready";
66
+ if (isOptional(title)) {
67
+ throw new Error("title is missing");
68
+ }
69
+
70
+ const subtitle: Nullable<string> = "ready";
71
+ if (isNullable(subtitle)) {
72
+ throw new Error("subtitle is missing");
73
+ }
74
+
75
+ // after the early guards both values are narrowed to string
76
+ title.toUpperCase();
77
+ subtitle.toUpperCase();
78
+
79
+ const tags: Nullable<string[]> = ["a", "b"];
80
+ if (!isArray(tags)) {
81
+ throw new Error("tags missing");
82
+ }
83
+
84
+ // tags is narrowed to string[]
85
+ tags.map((tag) => tag.toUpperCase());
86
+ ```
87
+
88
+ ### Global types
89
+
90
+ This opt-in global entry is available in the npm package surface. The JSR build
91
+ does not expose it because JSR rejects global augmentations in published
92
+ modules.
93
+
94
+ If you want the `typebuddy` utility types globally, opt in once in your project:
95
+
96
+ ```typescript
97
+ import type {} from "@murky-web/typebuddy/globals";
98
+ ```
99
+
100
+ That makes these names available globally:
101
+
102
+ - `Optional`
103
+ - `ResolveOptional`
104
+ - `Maybe`
105
+ - `ResolveMaybe`
106
+ - `Nullable`
107
+ - `ResolveNullable`
108
+ - `MaybePromise`
109
+ - `Success`
110
+ - `Failed`
111
+ - `JsonifiedValue`
112
+ - `Stringified`
113
+
114
+ ## API
115
+
116
+ ### Type Checking Functions
117
+
118
+ - `isObject(value: unknown): value is object`
119
+ - `isBoolean(value: unknown): value is boolean`
120
+ - `isNull(value: unknown): value is null`
121
+ - `isUndefined(value: unknown): value is undefined`
122
+ - `isOptional<T>(value: Optional<T>): value is undefined`
123
+ - `isMaybe<T>(value: Maybe<T>): value is null`
124
+ - `isNullable<T>(value: Nullable<T>): value is null | undefined`
125
+ - `isFunction(value: unknown): value is Function`
126
+ - `isPromise(value: unknown): value is Promise<unknown>`
127
+ - `isError(value: unknown): value is Error`
128
+ - `isDate(value: unknown): value is Date`
129
+ - `isRegExp(value: unknown): value is RegExp`
130
+ - `isSymbol(value: unknown): value is symbol`
131
+ - `isEmptyObject(value: unknown): value is Record<string, unknown>`
132
+ - `isInstanceOf<T>(value: unknown, constructor: { new (...args: unknown[]): T }): value is T`
133
+ - `isArray<T>(value: unknown): value is T[]`
134
+ - `isEmptyArray<T>(value: unknown): value is T[]`
135
+ - `isNumber(value: unknown): value is number`
136
+ - `isInteger(value: unknown): value is number`
137
+ - `isFloat(value: unknown): value is number`
138
+ - `isString(value: unknown): value is string`
139
+ - `isUuidString(input: unknown): input is string`
140
+ - `isUlidString(input: unknown): input is string`
141
+
142
+ ### Parsing Functions
143
+
144
+ - `parseNumber<T extends number, R extends Optional<T>>(value: unknown, defaultValue?: R): ResolveOptional<T, R>`
145
+ - `parseInteger(value: unknown, defaultValue?: Optional<number>): Optional<number>`
146
+ - `parseFloat<T extends number, R extends Optional<T>>(value: unknown, defaultValue?: R): ResolveOptional<T, R>`
147
+ - `parseString<T extends string, R extends Optional<T>>(value: unknown, defaultValue?: R): ResolveOptional<T, R>`
148
+ - `parseArray<T, R extends Optional<T[]>>(value: unknown, defaultValue?: R): ResolveOptional<T[], R>`
149
+ - `parseDomainName<T extends string, R extends Optional<T>>(url: string, defaultValue?: R): ResolveOptional<T, R>`
150
+
151
+ ### Utility Functions
152
+
153
+ - `getKeys<T extends Record<string, unknown>>(object: T): Array<keyof T>`
154
+ - `arrayContainsCommonValue<T>(array1: T[], array2: T[]): boolean`
155
+ - `isEmptyString(value: unknown): boolean`
156
+ - `isEmptyLike(value: unknown): boolean`
157
+ - `hasEmptyValues(value: unknown): boolean`
158
+
159
+ ### Types
160
+
161
+ - `Optional<T>`
162
+ - `ResolveOptional<T, R>`
163
+ - `Maybe<T>`
164
+ - `ResolveMaybe<T, R>`
165
+ - `Nullable<T>`
166
+ - `ResolveNullable<T, R>`
167
+ - `MaybePromise<T>`
168
+ - `Success<T>`
169
+ - `Failed<T>`
170
+ - `JsonifiedObject<T>`
171
+ - `JsonifiedValue<T>`
172
+ - `Stringified<T>`
173
+
174
+ ### Tooling Entries
175
+
176
+ - `@murky-web/typebuddy/oxlint`
177
+ - `@murky-web/typebuddy/globals`
package/biome/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ const resolveAsset = (asset: string): string =>
2
+ decodeURIComponent(new URL(asset, import.meta.url).pathname);
3
+
4
+ export const MaybePromiseRule: string = resolveAsset("./maybe_promise_rule.grit");
5
+ export const OptionalRule: string = resolveAsset("./optional_rule.gritt");
6
+ export const TryCatchAsyncRule: string = resolveAsset("./require-try-catch-async.grit");
@@ -0,0 +1,15 @@
1
+ or {
2
+ // 1. Replace Promise with MaybePromise in return types
3
+ `: Promise<$type>` => `: MaybePromise<$type>`,
4
+
5
+ // 2. Wrap return values in try blocks with { value, isError: false }
6
+ `try { $tryBody } catch ($err) { $catchBody }` where {
7
+ $tryBody <: contains `return $value` and {
8
+ $value <: not contains `{ isError: $isErrorValue }`
9
+ }
10
+ } => `try { $tryBody <: replace `return $value` => `return { value: $value, isError: false }` } catch ($err) { $catchBody }`,
11
+
12
+ // 3. Handle empty return statements in try blocks
13
+ `try { $tryBody } catch ($err) { $catchBody }` where {
14
+ $tryBody <: contains `return;`
15
+ } => `try { $tryBody <: replace `return;` => `return { value: undefined, isError: false };` } catch ($err) { $catchBody }`,
@@ -0,0 +1,11 @@
1
+ or {
2
+ // T | null -> Maybe<T>
3
+ `$type | null` where {
4
+ $type <: not contains `null | undefined`
5
+ } => `Maybe<$type>`,
6
+
7
+ // T | null | undefined -> Nullable<T>
8
+ `$type | null | undefined` where {
9
+ $type <: not contains `null | undefined`
10
+ } => `Nullable<$type>`
11
+ }
@@ -0,0 +1,3 @@
1
+ `$type | null | undefined` where {
2
+ $type <: not contains `null | undefined`
3
+ } => `Nullable<$type>`
@@ -0,0 +1,8 @@
1
+ or {
2
+ // T | null -> Maybe<T>
3
+ `$type | null` where { $type <: not contains `null | undefined` } => `Maybe<$type>`,
4
+ // T | null | undefined -> Nullable<T>
5
+ `$type | null | undefined` where { $type <: not contains `null | undefined` } => `Nullable<$type>`,
6
+ // T | undefined -> Optional<T>
7
+ `$type | undefined` where { $type <: not contains `undefined` } => `Optional<$type>`
8
+ }
@@ -0,0 +1,5 @@
1
+ or {
2
+ `async function $name($params) { $body }` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `async function $name($params) { try { $body } catch (err) { // handle error } }`,
3
+ `(async function($params) { $body })` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `(async function($params) { try { $body } catch (err) { // handle error } })`,
4
+ `(async ($params) => { $body })` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `(async ($params) => { try { $body } catch (err) { // handle error } })`
5
+ }
package/dist/biome.js ADDED
@@ -0,0 +1,7 @@
1
+ //#region biome/index.ts
2
+ const resolveAsset = (asset) => decodeURIComponent(new URL(asset, import.meta.url).pathname);
3
+ const MaybePromiseRule = resolveAsset("./maybe_promise_rule.grit");
4
+ const OptionalRule = resolveAsset("./optional_rule.gritt");
5
+ const TryCatchAsyncRule = resolveAsset("./require-try-catch-async.grit");
6
+ //#endregion
7
+ export { MaybePromiseRule, OptionalRule, TryCatchAsyncRule };
File without changes
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString } from "./type_helper.js";
2
+ export { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
@@ -0,0 +1,15 @@
1
+ or {
2
+ // 1. Replace Promise with MaybePromise in return types
3
+ `: Promise<$type>` => `: MaybePromise<$type>`,
4
+
5
+ // 2. Wrap return values in try blocks with { value, isError: false }
6
+ `try { $tryBody } catch ($err) { $catchBody }` where {
7
+ $tryBody <: contains `return $value` and {
8
+ $value <: not contains `{ isError: $isErrorValue }`
9
+ }
10
+ } => `try { $tryBody <: replace `return $value` => `return { value: $value, isError: false }` } catch ($err) { $catchBody }`,
11
+
12
+ // 3. Handle empty return statements in try blocks
13
+ `try { $tryBody } catch ($err) { $catchBody }` where {
14
+ $tryBody <: contains `return;`
15
+ } => `try { $tryBody <: replace `return;` => `return { value: undefined, isError: false };` } catch ($err) { $catchBody }`,
@@ -0,0 +1,11 @@
1
+ or {
2
+ // T | null -> Maybe<T>
3
+ `$type | null` where {
4
+ $type <: not contains `null | undefined`
5
+ } => `Maybe<$type>`,
6
+
7
+ // T | null | undefined -> Nullable<T>
8
+ `$type | null | undefined` where {
9
+ $type <: not contains `null | undefined`
10
+ } => `Nullable<$type>`
11
+ }
@@ -0,0 +1,3 @@
1
+ `$type | null | undefined` where {
2
+ $type <: not contains `null | undefined`
3
+ } => `Nullable<$type>`
@@ -0,0 +1,8 @@
1
+ or {
2
+ // T | null -> Maybe<T>
3
+ `$type | null` where { $type <: not contains `null | undefined` } => `Maybe<$type>`,
4
+ // T | null | undefined -> Nullable<T>
5
+ `$type | null | undefined` where { $type <: not contains `null | undefined` } => `Nullable<$type>`,
6
+ // T | undefined -> Optional<T>
7
+ `$type | undefined` where { $type <: not contains `undefined` } => `Optional<$type>`
8
+ }
package/dist/oxlint.js ADDED
@@ -0,0 +1,18 @@
1
+ import { rule } from "./rules/async_rule.js";
2
+ import { rule as rule$1 } from "./rules/maybe_promise_rule.js";
3
+ import { rule as rule$2 } from "./rules/maybe_rule.js";
4
+ import { rule as rule$3 } from "./rules/nullable_rule.js";
5
+ import { rule as rule$4 } from "./rules/optional_rule.js";
6
+ //#region oxlint/index.ts
7
+ const plugin = {
8
+ meta: { name: "typebuddy" },
9
+ rules: {
10
+ "prefer-maybe": rule$2,
11
+ "prefer-maybe-promise": rule$1,
12
+ "prefer-nullable": rule$3,
13
+ "prefer-optional": rule$4,
14
+ "require-try-catch": rule
15
+ }
16
+ };
17
+ //#endregion
18
+ export { plugin as default, plugin as typebuddyOxlintPlugin };
@@ -0,0 +1,5 @@
1
+ or {
2
+ `async function $name($params) { $body }` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `async function $name($params) { try { $body } catch (err) { // handle error } }`,
3
+ `(async function($params) { $body })` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `(async function($params) { try { $body } catch (err) { // handle error } })`,
4
+ `(async ($params) => { $body })` where { $body <: not contains `try { $tryBody } catch ($err) { $catchBody }` } => `(async ($params) => { try { $body } catch (err) { // handle error } })`
5
+ }
@@ -0,0 +1,58 @@
1
+ //#region rules/async_rule.ts
2
+ function hasTryCatch(nodes) {
3
+ return nodes.some((node) => {
4
+ return node.type === "TryStatement";
5
+ });
6
+ }
7
+ const rule = {
8
+ create(context) {
9
+ const sourceCode = context.getSourceCode();
10
+ function getIndentation(node) {
11
+ const firstLine = sourceCode.getText(node).split("\n")[0];
12
+ const match = /^\s*/.exec(firstLine);
13
+ return match ? match[0] : "";
14
+ }
15
+ function wrapInTryCatch(node) {
16
+ const body = Array.isArray(node.body) ? node.body : [];
17
+ const indent = getIndentation(node);
18
+ const innerIndent = `${indent} `;
19
+ return `{
20
+ ${indent}try {
21
+ ${body.map((statement) => {
22
+ return `${innerIndent}${sourceCode.getText(statement).replaceAll(/^\s*/gm, "")}`;
23
+ }).join("\n")}
24
+ ${indent}} catch (err) {
25
+ ${innerIndent}return FAILED_PROMISE;
26
+ ${indent}}
27
+ }`;
28
+ }
29
+ function checkFunction(node) {
30
+ if (node.async !== true) return;
31
+ const bodyNode = node.body;
32
+ if (!bodyNode || Array.isArray(bodyNode) || bodyNode.type !== "BlockStatement") return;
33
+ if (hasTryCatch(Array.isArray(bodyNode.body) ? bodyNode.body : [])) return;
34
+ context.report({
35
+ node,
36
+ messageId: "missingTryCatch",
37
+ fix(fixer) {
38
+ return fixer.replaceText(bodyNode, wrapInTryCatch(bodyNode));
39
+ }
40
+ });
41
+ }
42
+ return {
43
+ FunctionDeclaration: checkFunction,
44
+ FunctionExpression: checkFunction,
45
+ ArrowFunctionExpression: checkFunction
46
+ };
47
+ },
48
+ defaultOptions: [],
49
+ meta: {
50
+ type: "suggestion",
51
+ docs: { description: "Async functions should have a try-catch block returning FAILED_PROMISE." },
52
+ fixable: "code",
53
+ schema: [],
54
+ messages: { missingTryCatch: "Async functions must have a try-catch block returning FAILED_PROMISE." }
55
+ }
56
+ };
57
+ //#endregion
58
+ export { rule };
@@ -0,0 +1,132 @@
1
+ //#region rules/maybe_promise_rule.ts
2
+ function isNode(value) {
3
+ return typeof value === "object" && value !== null && "type" in value;
4
+ }
5
+ function isAsyncFunction(node) {
6
+ return node.async === true;
7
+ }
8
+ function hasTypeParameters(value) {
9
+ return isNode(value) && Array.isArray(value.params);
10
+ }
11
+ const rule = {
12
+ create(context) {
13
+ const sourceCode = context.getSourceCode();
14
+ function isTypeReference(node) {
15
+ return isNode(node) && node.type === "TSTypeReference";
16
+ }
17
+ function getTypeName(node) {
18
+ const typeName = node.typeName;
19
+ if (!isNode(typeName) || typeName.type !== "Identifier") return null;
20
+ return typeof typeName.name === "string" ? typeName.name : null;
21
+ }
22
+ function getTypeArgument(node) {
23
+ const typeArguments = node.typeArguments;
24
+ if (!hasTypeParameters(typeArguments)) return null;
25
+ const [firstParam] = typeArguments.params;
26
+ if (!isNode(firstParam)) return null;
27
+ return firstParam;
28
+ }
29
+ function getPromiseTypeArgument(node) {
30
+ if (getTypeName(node) !== "Promise") return null;
31
+ return getTypeArgument(node);
32
+ }
33
+ function checkReturnType(node) {
34
+ if (!isAsyncFunction(node)) return;
35
+ if (!isNode(node.returnType)) return;
36
+ const typeAnnotation = node.returnType.typeAnnotation;
37
+ if (!isTypeReference(typeAnnotation)) return;
38
+ if (getTypeName(typeAnnotation) !== "Promise") return;
39
+ const typeName = typeAnnotation.typeName;
40
+ context.report({
41
+ node: typeName,
42
+ messageId: "replaceWithMaybePromise",
43
+ fix(fixer) {
44
+ return fixer.replaceText(typeName, "MaybePromise");
45
+ }
46
+ });
47
+ }
48
+ function wrapReturnValue(node, isAsync, returnType) {
49
+ if (!isAsync) return;
50
+ const argument = node.argument;
51
+ if (!isNode(argument)) {
52
+ if (returnType?.type === "TSVoidKeyword") context.report({
53
+ node,
54
+ messageId: "returnVoidPromise",
55
+ fix(fixer) {
56
+ return fixer.replaceText(node, "return VOID_PROMISE");
57
+ }
58
+ });
59
+ return;
60
+ }
61
+ if (argument.type === "Identifier" && (argument.name === "VOID_PROMISE" || argument.name === "FAILED_PROMISE")) return;
62
+ if (!(argument.type === "ObjectExpression" && Array.isArray(argument.properties) && argument.properties.some((property) => isNode(property) && property.type === "Property" && isNode(property.key) && property.key.type === "Identifier" && property.key.name === "isError"))) context.report({
63
+ node: argument,
64
+ messageId: "wrapReturn",
65
+ fix(fixer) {
66
+ const argumentText = sourceCode.getText(argument);
67
+ return fixer.replaceText(argument, `{ value: ${argumentText}, isError: false }`);
68
+ }
69
+ });
70
+ }
71
+ function processTryCatch(node) {
72
+ let parent = node.parent;
73
+ let isAsync = false;
74
+ let parentFunction = null;
75
+ while (parent) {
76
+ if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
77
+ isAsync = parent.async === true;
78
+ parentFunction = parent;
79
+ break;
80
+ }
81
+ parent = parent.parent;
82
+ }
83
+ if (!isAsync || !parentFunction) return;
84
+ let returnType = null;
85
+ if (isNode(parentFunction.returnType)) {
86
+ const typeAnnotation = parentFunction.returnType.typeAnnotation;
87
+ if (isTypeReference(typeAnnotation)) returnType = getPromiseTypeArgument(typeAnnotation);
88
+ }
89
+ const blockBody = isNode(node.block) && Array.isArray(node.block.body) ? node.block.body : [];
90
+ for (const statement of blockBody) if (isNode(statement) && statement.type === "ReturnStatement") wrapReturnValue(statement, isAsync, returnType);
91
+ if (!isNode(node.handler) || !isNode(node.handler.body)) return;
92
+ const catchBody = Array.isArray(node.handler.body.body) ? node.handler.body.body.filter(isNode) : [];
93
+ if (!catchBody.some((statement) => statement.type === "ReturnStatement" && isNode(statement.argument) && statement.argument.type === "Identifier" && statement.argument.name === "FAILED_PROMISE")) context.report({
94
+ node: node.handler,
95
+ messageId: "returnFailedPromise",
96
+ fix(fixer) {
97
+ const lastStatement = catchBody.at(-1);
98
+ if (lastStatement?.type === "ReturnStatement" && isNode(lastStatement.argument) && lastStatement.argument.type === "ObjectExpression") return fixer.replaceText(lastStatement, "return FAILED_PROMISE");
99
+ const handler = node.handler;
100
+ if (!isNode(handler)) return null;
101
+ const bodyRange = handler.body;
102
+ if (!isNode(bodyRange) || !Array.isArray(bodyRange.range)) return null;
103
+ return fixer.insertTextBeforeRange([bodyRange.range[1] - 1, bodyRange.range[1] - 1], "return FAILED_PROMISE; ");
104
+ }
105
+ });
106
+ }
107
+ return {
108
+ FunctionDeclaration: checkReturnType,
109
+ FunctionExpression: checkReturnType,
110
+ ArrowFunctionExpression: checkReturnType,
111
+ TSDeclareFunction: checkReturnType,
112
+ TSFunctionType: checkReturnType,
113
+ TSMethodSignature: checkReturnType,
114
+ TryStatement: processTryCatch
115
+ };
116
+ },
117
+ defaultOptions: [],
118
+ meta: {
119
+ type: "suggestion",
120
+ docs: { description: "Ensure async functions' return values follow the MaybePromise pattern." },
121
+ fixable: "code",
122
+ schema: [],
123
+ messages: {
124
+ replaceWithMaybePromise: "Use 'MaybePromise' instead of 'Promise' as the return type in async functions.",
125
+ wrapReturn: "Wrap return value with { value: value, isError: false }.",
126
+ returnVoidPromise: "Return VOID_PROMISE for async functions with Promise<void> return type.",
127
+ returnFailedPromise: "Return FAILED_PROMISE in catch block of async functions."
128
+ }
129
+ }
130
+ };
131
+ //#endregion
132
+ export { rule };
@@ -0,0 +1,34 @@
1
+ //#region rules/maybe_rule.ts
2
+ const rule = {
3
+ create(context) {
4
+ return { TSUnionType(node) {
5
+ const types = node.types;
6
+ const sourceCode = context.getSourceCode();
7
+ const nullType = types.find((typeNode) => typeNode.type === "TSNullKeyword");
8
+ const undefinedType = types.find((typeNode) => typeNode.type === "TSUndefinedKeyword");
9
+ const otherTypes = types.filter((typeNode) => typeNode.type !== "TSNullKeyword" && typeNode.type !== "TSUndefinedKeyword");
10
+ if (nullType && !undefinedType && otherTypes.length === 1) {
11
+ const typeText = sourceCode.getText(otherTypes[0]);
12
+ context.report({
13
+ node,
14
+ messageId: "useMaybe",
15
+ data: { type: typeText },
16
+ fix(fixer) {
17
+ return fixer.replaceText(node, `Maybe<${typeText}>`);
18
+ }
19
+ });
20
+ }
21
+ } };
22
+ },
23
+ defaultOptions: [],
24
+ meta: {
25
+ type: "suggestion",
26
+ hasSuggestions: true,
27
+ docs: { description: "Use Maybe<T> for T | null" },
28
+ fixable: "code",
29
+ schema: [],
30
+ messages: { useMaybe: "Use Maybe<{{type}}> instead of {{type}} | null" }
31
+ }
32
+ };
33
+ //#endregion
34
+ export { rule };
@@ -0,0 +1,35 @@
1
+ //#region rules/nullable_rule.ts
2
+ const rule = {
3
+ create(context) {
4
+ return { TSUnionType(node) {
5
+ const types = node.types;
6
+ if (types.length !== 3) return;
7
+ const sourceCode = context.getSourceCode();
8
+ const nullType = types.find((typeNode) => typeNode.type === "TSNullKeyword");
9
+ const undefinedType = types.find((typeNode) => typeNode.type === "TSUndefinedKeyword");
10
+ const otherType = types.find((typeNode) => typeNode.type !== "TSNullKeyword" && typeNode.type !== "TSUndefinedKeyword");
11
+ if (nullType && undefinedType && otherType) {
12
+ const typeText = sourceCode.getText(otherType);
13
+ context.report({
14
+ node,
15
+ messageId: "useNullable",
16
+ data: { type: typeText },
17
+ fix(fixer) {
18
+ return fixer.replaceText(node, `Nullable<${typeText}>`);
19
+ }
20
+ });
21
+ }
22
+ } };
23
+ },
24
+ defaultOptions: [],
25
+ meta: {
26
+ type: "suggestion",
27
+ hasSuggestions: true,
28
+ docs: { description: "Use Nullable<T> instead of T | null | undefined" },
29
+ fixable: "code",
30
+ schema: [],
31
+ messages: { useNullable: "Use Nullable<{{type}}> instead of {{type}} | null | undefined" }
32
+ }
33
+ };
34
+ //#endregion
35
+ export { rule };
@@ -0,0 +1,31 @@
1
+ //#region rules/optional_rule.ts
2
+ const rule = {
3
+ create(context) {
4
+ return { TSUnionType(node) {
5
+ if (node.types.length !== 2) return;
6
+ const undefinedType = node.types.find((typeNode) => typeNode.type === "TSUndefinedKeyword");
7
+ const otherType = node.types.find((typeNode) => typeNode.type !== "TSUndefinedKeyword");
8
+ if (undefinedType && otherType) {
9
+ const typeText = context.getSourceCode().getText(otherType);
10
+ context.report({
11
+ node,
12
+ messageId: "preferOptional",
13
+ data: { type: typeText },
14
+ fix(fixer) {
15
+ return fixer.replaceText(node, `Optional<${typeText}>`);
16
+ }
17
+ });
18
+ }
19
+ } };
20
+ },
21
+ defaultOptions: [],
22
+ meta: {
23
+ type: "suggestion",
24
+ docs: { description: "Enforce using Optional<T> instead of T | undefined" },
25
+ fixable: "code",
26
+ schema: [],
27
+ messages: { preferOptional: "Use Optional<{{type}}> instead of {{type}} | undefined" }
28
+ }
29
+ };
30
+ //#endregion
31
+ export { rule };