From f13151f464a28ac8f42f8db5b0caa75ef42a4e38 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 23 Oct 2024 10:29:27 +0200 Subject: [PATCH] chore: next version (#1079) * chore: change CI target for next * chore: add next branch to CI targets * feat: target ESLint v9 * Tighten type checking config (#1071) * Use more restrictive type signatures * Don't rely on broken expect.error() * chore: report node 18+ support (#1081) * chore: update documentation for eslint ignore --------- Co-authored-by: Gil Pedersen --- .eslintignore | 9 --- .github/workflows/ci-module.yml | 5 +- API.md | 15 +++-- eslint.config.js | 20 +++++++ lib/linter/.eslintrc.js | 6 +- lib/linter/index.js | 55 ++++++++++++++----- lib/modules/coverage.js | 40 ++++++++++++-- lib/modules/lint.js | 2 +- lib/modules/transform.js | 2 +- lib/modules/types.js | 2 + lib/modules/typescript.js | 2 +- lib/runner.js | 2 + package.json | 23 ++++---- test/cli.js | 3 +- test/coverage.js | 6 +- .../{.eslintrc.js => eslint.config.js} | 8 ++- test/coverage/test-folder/test-name.js | 4 +- test/lint/eslint/esm/.eslintrc.cjs | 13 ----- test/lint/eslint/esm/eslint.config.cjs | 22 ++++++++ test/lint/eslint/typescript/.eslintrc.cjs | 5 -- test/lint/eslint/typescript/eslint.config.cjs | 12 ++++ test/lint/eslint/with_config/.eslintignore | 1 - test/lint/eslint/with_config/.eslintrc.js | 9 --- test/lint/eslint/with_config/eslint.config.js | 15 +++++ test/linters.js | 7 +-- test/reporters.js | 1 + test/runner.js | 2 + test/types.js | 12 ++++ test/types/errors/lib/index.d.ts | 11 ++++ test/types/errors/test/restrict.ts | 15 +++++ 30 files changed, 233 insertions(+), 96 deletions(-) delete mode 100644 .eslintignore create mode 100644 eslint.config.js rename test/coverage/test-folder/{.eslintrc.js => eslint.config.js} (65%) delete mode 100644 test/lint/eslint/esm/.eslintrc.cjs create mode 100644 test/lint/eslint/esm/eslint.config.cjs delete mode 100644 test/lint/eslint/typescript/.eslintrc.cjs create mode 100644 test/lint/eslint/typescript/eslint.config.cjs delete mode 100644 test/lint/eslint/with_config/.eslintignore delete mode 100644 test/lint/eslint/with_config/.eslintrc.js create mode 100644 test/lint/eslint/with_config/eslint.config.js create mode 100644 test/types/errors/test/restrict.ts diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7a280b04..00000000 --- a/.eslintignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules/ -test_runner/ -test/coverage/ -test/cli/ -test/cli_*/ -test/lint/ -test/override/ -test/plan/ -test/transform/ diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 54426ca2..44369c8b 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -4,11 +4,10 @@ on: push: branches: - master + - next pull_request: workflow_dispatch: jobs: test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master - with: - min-node-version: 14 + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21 diff --git a/API.md b/API.md index 0b6d6dc0..df798668 100755 --- a/API.md +++ b/API.md @@ -632,10 +632,17 @@ Your project's eslint configuration will now extend the default **lab** configur ### Ignoring files in linting -Since [eslint](http://eslint.org/) is used to lint, you can create an `.eslintignore` containing paths to be ignored: -``` -node_modules/* -**/vendor/*.js +Since [eslint](http://eslint.org/) is used to lint, if you don't already have an `eslint.config.{js|cjs|mjs|ts|mts|cts}` you can create one, +and add an `ignores` rule containing paths to be ignored. Here is an example preserving default hapi rules: +```javascript +import HapiPlugin from '@hapi/eslint-plugin'; + +export default [ + { + ignores: ['node_modules/*', '**/vendor/*.js'], + }, + ...HapiPlugin.configs.module, +]; ``` ### Only run linting diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..efedefd6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,20 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [ + { + ignores: [ + 'node_modules/', + 'test_runner/', + 'test/coverage/', + 'test/cli/', + 'test/cli_*/', + 'test/lint/', + 'test/override/', + 'test/plan/', + 'test/transform/' + ] + }, + ...HapiPlugin.configs.module +]; diff --git a/lib/linter/.eslintrc.js b/lib/linter/.eslintrc.js index b2d74046..e7b26db8 100755 --- a/lib/linter/.eslintrc.js +++ b/lib/linter/.eslintrc.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = { - extends: 'plugin:@hapi/module' -}; +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [...HapiPlugin.configs.module]; diff --git a/lib/linter/index.js b/lib/linter/index.js index 9f25bbe2..d1ec4a44 100755 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -1,7 +1,6 @@ 'use strict'; const Fs = require('fs'); -const Path = require('path'); const Eslint = require('eslint'); const Hoek = require('@hapi/hoek'); @@ -18,31 +17,50 @@ exports.lint = async function () { const options = process.argv[2] ? JSON.parse(process.argv[2]) : undefined; - if (!Fs.existsSync('.eslintrc.js') && - !Fs.existsSync('.eslintrc.cjs') && // Needed for projects with "type": "module" - !Fs.existsSync('.eslintrc.yaml') && - !Fs.existsSync('.eslintrc.yml') && - !Fs.existsSync('.eslintrc.json') && - !Fs.existsSync('.eslintrc')) { - configuration.overrideConfigFile = Path.join(__dirname, '.eslintrc.js'); + let usingDefault = false; + + if (!Fs.existsSync('eslint.config.js') && + !Fs.existsSync('eslint.config.cjs') && + !Fs.existsSync('eslint.config.mjs') && + !Fs.existsSync('eslint.config.ts') && + !Fs.existsSync('eslint.config.mts') && + !Fs.existsSync('eslint.config.cts')) { + // No configuration file found, using the default one + usingDefault = true; + configuration.baseConfig = require('./.eslintrc.js'); + configuration.overrideConfigFile = true; } if (options) { Hoek.merge(configuration, options, true, false); } - if (!configuration.extensions) { - configuration.extensions = ['.js', '.cjs', '.mjs']; + // Only the default configuration should be altered, otherwise the user's configuration should be used as is + if (usingDefault) { + if (!configuration.extensions) { + const extensions = ['js', 'cjs', 'mjs']; - if (configuration.typescript) { - configuration.extensions.push('.ts'); + if (configuration.typescript) { + extensions.push('ts'); + } + + configuration.baseConfig.unshift({ + files: extensions.map((ext) => `**/*.${ext}`) + }); } - } - if (configuration.typescript) { - delete configuration.typescript; + if (configuration.ignores) { + configuration.baseConfig.unshift({ + ignores: configuration.ignores + }); + } } + delete configuration.extensions; + delete configuration.typescript; + delete configuration.ignores; + + let results; try { const eslint = new Eslint.ESLint(configuration); @@ -66,6 +84,13 @@ exports.lint = async function () { transformed.errors = result.messages.map((err) => { + if (err.messageTemplate === 'all-matched-files-ignored') { + return { + severity: 'ERROR', + message: err.message + }; + } + return { line: err.line, severity: err.severity === 1 ? 'WARNING' : 'ERROR', diff --git a/lib/modules/coverage.js b/lib/modules/coverage.js index 60c323bb..3753897b 100755 --- a/lib/modules/coverage.js +++ b/lib/modules/coverage.js @@ -16,8 +16,7 @@ const SourceMap = require('../source-map'); const Transform = require('./transform'); const internals = { - _state: Symbol.for('@hapi/lab/coverage/_state'), - eslint: new ESLint.ESLint({ baseConfig: Eslintrc }) + _state: Symbol.for('@hapi/lab/coverage/_state') }; @@ -111,7 +110,7 @@ internals.prime = function (extension, ctx) { require.extensions[extension] = function (localModule, filename) { // We never want to instrument eslint configs in order to avoid infinite recursion - if (Path.basename(filename, extension) !== '.eslintrc') { + if (!['.eslintrc', 'eslint.config'].includes(Path.basename(filename, extension))) { for (let i = 0; i < internals.state.patterns.length; ++i) { if (internals.state.patterns[i].test(filename.replace(/\\/g, '/'))) { return localModule._compile(internals.instrument(filename, ctx), filename); @@ -761,11 +760,40 @@ internals.file = async function (filename, data, options) { internals.context = async (options) => { + const filePath = Path.join(options.coveragePath || '', 'x.js'); + let calculated; + // The parserOptions are shared by all files for coverage purposes, based on // the effective eslint config for a hypothetical file {coveragePath}/x.js - const { parserOptions } = await internals.eslint.calculateConfigForFile( - Path.join(options.coveragePath || '', 'x.js') - ); + try { + // Let's try first with eslint's native configuration detection + const eslint = new ESLint.ESLint({ + ignore: false + }); + + calculated = await eslint.calculateConfigForFile(filePath); + } + catch (err) { + /* $lab:coverage:off$ */ + if (err.messageTemplate !== 'config-file-missing') { + throw err; + } + + // If the eslint config file is missing, we'll use the one provided by lab + const eslint = new ESLint.ESLint({ + overrideConfig: Eslintrc, + overrideConfigFile: true, + ignore: false + }); + + calculated = await eslint.calculateConfigForFile(filePath); + /* $lab:coverage:on$ */ + } + + const parserOptions = { + ...calculated.languageOptions, + ...calculated.languageOptions?.parserOptions + }; return { parserOptions }; }; diff --git a/lib/modules/lint.js b/lib/modules/lint.js index 2a7ad74c..8413b189 100755 --- a/lib/modules/lint.js +++ b/lib/modules/lint.js @@ -20,7 +20,7 @@ exports.lint = function (settings) { try { linterOptions = JSON.parse(settings['lint-options'] || '{}'); } - catch (err) { + catch { return reject(new Error('lint-options could not be parsed')); } diff --git a/lib/modules/transform.js b/lib/modules/transform.js index d80b3a62..55587cf1 100755 --- a/lib/modules/transform.js +++ b/lib/modules/transform.js @@ -73,7 +73,7 @@ exports.retrieveFile = function (path) { try { contents = Fs.readFileSync(path, 'utf8'); } - catch (e) { + catch { contents = null; } diff --git a/lib/modules/types.js b/lib/modules/types.js index 64418616..e173f73b 100755 --- a/lib/modules/types.js +++ b/lib/modules/types.js @@ -15,6 +15,8 @@ const Utils = require('../utils'); const internals = { compiler: { strict: true, + noUncheckedIndexedAccess: true, + exactOptionalPropertyTypes: true, jsx: Ts.JsxEmit.React, lib: ['lib.es2020.d.ts'], module: Ts.ModuleKind.CommonJS, diff --git a/lib/modules/typescript.js b/lib/modules/typescript.js index a004ae1a..9b4072f8 100755 --- a/lib/modules/typescript.js +++ b/lib/modules/typescript.js @@ -14,7 +14,7 @@ internals.transform = function (content, fileName) { try { var { config, error } = Typescript.readConfigFile(configFile, Typescript.sys.readFile); } - catch (err) { + catch { throw new Error(`Cannot find a tsconfig file for ${fileName}`); } diff --git a/lib/runner.js b/lib/runner.js index 6369f4e8..d1c5510d 100755 --- a/lib/runner.js +++ b/lib/runner.js @@ -13,10 +13,12 @@ const internals = {}; // Prevent libraries like Sinon from clobbering global time functions +/* eslint-disable no-redeclare */ const Date = global.Date; const setTimeout = global.setTimeout; const clearTimeout = global.clearTimeout; const setImmediate = global.setImmediate; +/* eslint-enable no-redeclare */ Error.stackTraceLimit = Infinity; // Set Error stack size diff --git a/package.json b/package.json index 64d9b376..3d1cff57 100755 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", + "engines": { + "node": ">=18" + }, "keywords": [ "test", "runner" @@ -13,19 +16,14 @@ "bin/lab", "lib" ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, "dependencies": { "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.0", + "@babel/eslint-parser": "^7.25.1", "@hapi/bossy": "^6.0.0", - "@hapi/eslint-plugin": "^6.0.0", + "@hapi/eslint-plugin": "^7.0.0", "@hapi/hoek": "^11.0.2", "diff": "^5.0.0", - "eslint": "8.x.x", + "eslint": "9.x.x", "find-rc": "4.x.x", "globby": "^11.1.0", "handlebars": "4.x.x", @@ -37,8 +35,8 @@ "will-call": "1.x.x" }, "peerDependencies": { - "@hapi/eslint-plugin": "^6.0.0", - "typescript": ">=3.6.5" + "@hapi/eslint-plugin": "^7.0.0", + "typescript": ">=4.4.0" }, "peerDependenciesMeta": { "typescript": { @@ -48,13 +46,14 @@ "devDependencies": { "@hapi/code": "^9.0.0", "@hapi/somever": "^4.0.0", + "@types/eslint": "^9.6.0", "@types/node": "^18.11.17", - "@typescript-eslint/parser": "^5.62.0", "cpr": "3.x.x", "lab-event-reporter": "1.x.x", "semver": "7.x.x", "tsconfig-paths": "^4.0.0", - "typescript": "^4.5.4" + "typescript": "^4.5.4", + "typescript-eslint": "^8.1.0" }, "bin": { "lab": "./bin/lab" diff --git a/test/cli.js b/test/cli.js index 8ee50564..9a94f691 100755 --- a/test/cli.js +++ b/test/cli.js @@ -3,6 +3,7 @@ // Load modules const ChildProcess = require('child_process'); +// eslint-disable-next-line no-redeclare const Crypto = require('crypto'); const Fs = require('fs'); const Http = require('http'); @@ -702,7 +703,7 @@ describe('CLI', () => { try { await unlink(outputPath); } - catch (err) { + catch { // Error is ok here } diff --git a/test/coverage.js b/test/coverage.js index 2cf34aab..f55b825b 100755 --- a/test/coverage.js +++ b/test/coverage.js @@ -566,19 +566,19 @@ describe('Coverage', () => { it('sorts file paths in report', async () => { const files = global.__$$labCov.files; - const paths = ['/a/b', '/a/b/c', '/a/c/b', '/a/c', '/a/b/c', '/a/b/a']; + const paths = ['./a/b', './a/b/c', './a/c/b', './a/c', './a/b/c', './a/b/a']; paths.forEach((path) => { files[path] = { source: [] }; }); - const cov = await Lab.coverage.analyze({ coveragePath: '/a' }); + const cov = await Lab.coverage.analyze({ coveragePath: './a' }); const sorted = cov.files.map((file) => { return file.filename; }); - expect(sorted).to.equal(['/a/b', '/a/c', '/a/b/a', '/a/b/c', '/a/c/b']); + expect(sorted).to.equal(['./a/b', './a/c', './a/b/a', './a/b/c', './a/c/b']); }); }); diff --git a/test/coverage/test-folder/.eslintrc.js b/test/coverage/test-folder/eslint.config.js similarity index 65% rename from test/coverage/test-folder/.eslintrc.js rename to test/coverage/test-folder/eslint.config.js index f20a20eb..7fa11d92 100644 --- a/test/coverage/test-folder/.eslintrc.js +++ b/test/coverage/test-folder/eslint.config.js @@ -1,9 +1,11 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + // this is a deliberately unused function that will reduce coverage percentage // if it ends up getting instrumented, giving us something to assert against const unusedMethod = () => { console.log('hello world') } -module.exports = { - extends: 'plugin:@hapi/module' -} +module.exports = [...HapiPlugin.configs.module] diff --git a/test/coverage/test-folder/test-name.js b/test/coverage/test-folder/test-name.js index 84e87723..d8e4c397 100644 --- a/test/coverage/test-folder/test-name.js +++ b/test/coverage/test-folder/test-name.js @@ -1,7 +1,7 @@ 'use strict'; // Load modules - +const EslintConfig = require('./eslint.config'); // Declare internals @@ -10,5 +10,5 @@ const internals = {}; exports.method = function () { - return; + return EslintConfig; }; diff --git a/test/lint/eslint/esm/.eslintrc.cjs b/test/lint/eslint/esm/.eslintrc.cjs deleted file mode 100644 index 0bda9be6..00000000 --- a/test/lint/eslint/esm/.eslintrc.cjs +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = { - parserOptions: { - sourceType: 'module' - }, - overrides: [ - { - files: ['*.cjs'], - parserOptions: { sourceType: 'script' } - } - ] -}; diff --git a/test/lint/eslint/esm/eslint.config.cjs b/test/lint/eslint/esm/eslint.config.cjs new file mode 100644 index 00000000..3c14a73f --- /dev/null +++ b/test/lint/eslint/esm/eslint.config.cjs @@ -0,0 +1,22 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [ + ...HapiPlugin.configs.module, + { + languageOptions: { + parserOptions: { + sourceType: 'module' + } + } + }, + { + files: ['*.cjs'], + languageOptions: { + parserOptions: { + sourceType: 'script' + } + } + } +]; diff --git a/test/lint/eslint/typescript/.eslintrc.cjs b/test/lint/eslint/typescript/.eslintrc.cjs deleted file mode 100644 index 968d9a06..00000000 --- a/test/lint/eslint/typescript/.eslintrc.cjs +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - parser: '@typescript-eslint/parser' -}; diff --git a/test/lint/eslint/typescript/eslint.config.cjs b/test/lint/eslint/typescript/eslint.config.cjs new file mode 100644 index 00000000..c97aa2f3 --- /dev/null +++ b/test/lint/eslint/typescript/eslint.config.cjs @@ -0,0 +1,12 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); +const TsESLint = require('typescript-eslint'); + +module.exports = TsESLint.config( + { + files: ['**/*.ts'] + }, + ...HapiPlugin.configs.module, + TsESLint.configs.base +); diff --git a/test/lint/eslint/with_config/.eslintignore b/test/lint/eslint/with_config/.eslintignore deleted file mode 100644 index ec79d651..00000000 --- a/test/lint/eslint/with_config/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -*.ignore.* \ No newline at end of file diff --git a/test/lint/eslint/with_config/.eslintrc.js b/test/lint/eslint/with_config/.eslintrc.js deleted file mode 100644 index ca8a9023..00000000 --- a/test/lint/eslint/with_config/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = { - 'rules': { - 'eol-last': 2, - 'no-unused-vars': 0, - 'no-undef': 0 - } -}; diff --git a/test/lint/eslint/with_config/eslint.config.js b/test/lint/eslint/with_config/eslint.config.js new file mode 100644 index 00000000..99b3313f --- /dev/null +++ b/test/lint/eslint/with_config/eslint.config.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = [ + { + ignores: ['*.ignore.*'] + }, + { + 'rules': { + 'eol-last': 2, + 'no-unused-vars': 0, + 'no-undef': 0 + } + } +]; + diff --git a/test/linters.js b/test/linters.js index 5888ddcb..0dc67706 100755 --- a/test/linters.js +++ b/test/linters.js @@ -88,7 +88,7 @@ describe('Linters - eslint', () => { { line: 12, severity: 'WARNING', message: 'eol-last - Newline required at end of file but not found.' } ]); - const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, '.eslintrc.cjs')); + const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, 'eslint.config.cjs')); expect(checkedCjsFile.errors).to.be.empty(); }); @@ -131,7 +131,6 @@ describe('Linters - eslint', () => { const checkedFile = eslintResults.find(({ filename }) => filename.endsWith('.ts')); expect(checkedFile).to.include({ filename: Path.join(path, 'fail.ts') }); expect(checkedFile.errors).to.include([ - { line: 1, severity: 'ERROR', message: `strict - Use the global form of 'use strict'.` }, { line: 6, severity: 'ERROR', message: 'indent - Expected indentation of 4 spaces but found 1 tab.' }, { line: 6, severity: 'ERROR', message: 'semi - Missing semicolon.' } ]); @@ -195,7 +194,7 @@ describe('Linters - eslint', () => { it('should pass options and not find any files', async () => { - const lintOptions = JSON.stringify({ extensions: ['.jsx'] }); + const lintOptions = JSON.stringify({ extensions: ['.jsx'], ignores: ['**/*.js'] }); const path = Path.join(__dirname, 'lint', 'eslint', 'basic'); const result = await Linters.lint({ lintingPath: path, linter: 'eslint', 'lint-options': lintOptions }); @@ -203,7 +202,7 @@ describe('Linters - eslint', () => { const eslintResults = result.lint; expect(eslintResults).to.have.length(1); - expect(eslintResults[0].errors[0].message).to.contain('No files'); + expect(eslintResults[0].errors[0].message).to.contain('All files matched by \'.\' are ignored.'); }); it('should fix lint rules when --lint-fix used', async (flags) => { diff --git a/test/reporters.js b/test/reporters.js index 123491e2..9115a127 100755 --- a/test/reporters.js +++ b/test/reporters.js @@ -1,5 +1,6 @@ 'use strict'; +// eslint-disable-next-line no-redeclare const Crypto = require('crypto'); const Fs = require('fs/promises'); const Os = require('os'); diff --git a/test/runner.js b/test/runner.js index 218dbef9..83a16d49 100755 --- a/test/runner.js +++ b/test/runner.js @@ -25,9 +25,11 @@ const expect = Code.expect; // save references to timer globals +/* eslint-disable no-redeclare */ const setTimeout = global.setTimeout; const clearTimeout = global.clearTimeout; const setImmediate = global.setImmediate; +/* eslint-enable no-redeclare */ describe('Runner', () => { diff --git a/test/types.js b/test/types.js index b3ee5538..48fc376a 100755 --- a/test/types.js +++ b/test/types.js @@ -98,6 +98,18 @@ describe('Types', () => { line: 3, column: 4 }, + { + filename: 'test/restrict.ts', + message: `Type 'undefined' is not assignable to type 'string' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.`, + line: 10, + column: 0 + }, + { + filename: 'test/restrict.ts', + message: `'unchecked.b' is possibly 'undefined'.`, + line: 15, + column: 0 + }, { filename: 'test/syntax.ts', message: `')' expected.`, diff --git a/test/types/errors/lib/index.d.ts b/test/types/errors/lib/index.d.ts index 42453aa8..bb5db30d 100755 --- a/test/types/errors/lib/index.d.ts +++ b/test/types/errors/lib/index.d.ts @@ -8,3 +8,14 @@ export default add; export const sample: { readonly x: string }; export function hasProperty(property: { name: string }): boolean; + +export interface UsesExactOptionalPropertyTypes { + a?: boolean | undefined; + b?: string; +} + +export interface UncheckedIndexedAccess { + a: UsesExactOptionalPropertyTypes; + + [prop: string]: UsesExactOptionalPropertyTypes; +} diff --git a/test/types/errors/test/restrict.ts b/test/types/errors/test/restrict.ts new file mode 100644 index 00000000..4705a9d0 --- /dev/null +++ b/test/types/errors/test/restrict.ts @@ -0,0 +1,15 @@ +import * as Lab from '../../../..'; +import type { UncheckedIndexedAccess, UsesExactOptionalPropertyTypes } from '../lib/index'; + +const { expect } = Lab.types; + +const exact: UsesExactOptionalPropertyTypes = { a: true }; + +exact.a = undefined; +exact.b = 'ok'; +exact.b = undefined; // Fails + +const unchecked: UncheckedIndexedAccess = { a: exact, b: {} }; + +unchecked.a.a; +unchecked.b.a; // Fails