Skip to content

Commit

Permalink
Nullish helper types (#7016)
Browse files Browse the repository at this point in the history
* creates nullish helper types

* adds possible typeguards

* updates types based on feedback

* adds NonNullish and Nullishable types as suggestions

* further refinement

* further refining types

* adds tests for handlers

* swap T extends Nullish ternary for Exclude

* move assumeNotNull_UNSAFE -> assumeNotNullish_UNSAFE and colocated in nullish utils

* adds preferNullish local eslint rule

* adds preferNullishable local eslint rule

* removes console log

* drops unnecessary typeguards
  • Loading branch information
grahamlangford authored Dec 11, 2023
1 parent 043bb3b commit 873724f
Show file tree
Hide file tree
Showing 23 changed files with 572 additions and 59 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ module.exports = {
"local-rules/noInvalidDataTestId": "error",
"local-rules/noExpressionLiterals": "error",
"local-rules/notBothLabelAndLockableProps": "error",
"local-rules/preferNullish": "warn",
"local-rules/preferNullishable": "warn",
"import/no-restricted-paths": [
"warn",
{
Expand Down
47 changes: 47 additions & 0 deletions eslint-local-rules/preferNullish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Encourages using Nullish type instead of null | undefined",
category: "Best Practices",
recommended: true,
},
},
create(context) {
return {
TSUnionType(node) {
// If the union type is not exactly two types, then it can't be null | undefined
// Prefer Nullishable<T> instead of T | Nullish when there are more than two types
if (node.types.length !== 2) return;

const types = node.types.map((type) => type.type);
if (
types.includes("TSNullKeyword") &&
types.includes("TSUndefinedKeyword")
) {
context.report({
node,
message: "Consider using Nullish instead of null | undefined",
});
}
},
};
},
};
59 changes: 59 additions & 0 deletions eslint-local-rules/preferNullish.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

const rule = require("./preferNullish");
const { RuleTester } = require("@typescript-eslint/rule-tester");

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2021,
sourceType: "module",
ecmaFeatures: {},
},
});

ruleTester.run("preferNullish", rule, {
valid: [
"let x: Nullish;",
"let y: string | number;",
"let z: null",
"let a: undefined;",
"type Foo = string | number",
"type Bar = null | (() => void)",
// See preferNullishable for the following test cases
"type Foo = string | null | undefined;",
"let y: string | undefined | null;",
],
invalid: [
{
code: "let x: null | undefined;",
errors: [
{
message: "Consider using Nullish instead of null | undefined",
},
],
},
{
code: "type Bar = null | undefined;",
errors: [
{
message: "Consider using Nullish instead of null | undefined",
},
],
},
],
});
61 changes: 61 additions & 0 deletions eslint-local-rules/preferNullishable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Encourages using Nullishable<T> instead of T | Nullish",
category: "Best Practices",
recommended: true,
},
},
create(context) {
return {
TSUnionType(node) {
if (node.types.length < 2) {
return;
}

const types = node.types.map((type) => type.type);

if (
types.includes("TSNullKeyword") &&
types.includes("TSUndefinedKeyword")
) {
context.report({
node,
message:
"Consider using Nullishable<T> instead of T | null | undefined",
});
}

const isNullishUnion = node.types.find(
(type) =>
type.type === "TSTypeReference" && type.typeName.name === "Nullish",
);

if (isNullishUnion) {
context.report({
node,
message: "Consider using Nullishable<T> instead of T | Nullish",
});
}
},
};
},
};
67 changes: 67 additions & 0 deletions eslint-local-rules/preferNullishable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

const rule = require("./preferNullishable");
const { RuleTester } = require("@typescript-eslint/rule-tester");

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2021,
sourceType: "module",
ecmaFeatures: {},
},
});

ruleTester.run("preferNullishable", rule, {
valid: [
"let x: Nullish;",
"let y: string | number;",
"let z: null",
"let a: undefined;",
"type Foo = string | number",
"type Bar = null | (() => void)",
"type Baz = Foo | Bar",
],
invalid: [
{
code: "let x: string | null | undefined;",
errors: [
{
message:
"Consider using Nullishable<T> instead of T | null | undefined",
},
],
},
{
code: "type Foo = string | number | Nullish;",
errors: [
{
message: "Consider using Nullishable<T> instead of T | Nullish",
},
],
},
{
code: "type Bar = string | number | null | undefined;",
errors: [
{
message:
"Consider using Nullishable<T> instead of T | null | undefined",
},
],
},
],
});
Loading

0 comments on commit 873724f

Please sign in to comment.