最早接觸到 TypeScript 開發是透過 VSCode 的 Your First Extension 範例。教學透過 Yeoman 使用 VS Code Extension Generator 產生。預設已經自帶 ESlint 檢查。所以也沒特別在意就直接開發。在近期我回頭檢視相關的規則檔,才發現預設的規則其實非常寬鬆。

{
	"rules": {
		"@typescript-eslint/member-delimiter-style": [
			"warn",
			{
				"multiline": {
					"delimiter": "semi",
					"requireLast": true
				},
				"singleline": {
					"delimiter": "semi",
					"requireLast": false
				}
			}
		],
		"@typescript-eslint/naming-convention": "warn",
		"@typescript-eslint/no-unused-expressions": "warn",
		"@typescript-eslint/semi": [
			"warn",
			"always"
		],
		"curly": "warn",
		"eqeqeq": [
			"warn",
			"always"
		],
		"no-redeclare": "warn",
		"no-throw-literal": "warn",
		"no-unused-expressions": "warn",
		"semi": "warn"
	}
}

所以以往開發很少注意到寫法上有潛在的問題。畢竟也初學 TypeScript,編譯能過就好。但最近回頭加入 ESlint 驗證的 CI/CD,才根據 ESLint 官方建議的設定加入底下的擴充。

{
	extends: [
		'eslint:recommended',
		'plugin:@typescript-eslint/recommended-type-checked',
		'plugin:@typescript-eslint/stylistic-type-checked',
	],
}

想當然爾冒出了很多錯誤需要修正,這邊列出那些潛越的規則:

eslint@typescript-eslint/no-floating-promises

使用 promise 需要被 await,以及合理的配置錯誤處理。

// 錯誤, Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.
vscode.window.showWarningMessage('Laravel Goto: unidentified string.');

// 正確
await vscode.window.showWarningMessage('Laravel Goto: unidentified string.');

eslint@typescript-eslint/prefer-nullish-coalescing

建議用 Nullish Coalescing Operator (??) 取代 || 的寫法。

// 錯誤, Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
path = await vscode.window.showQuickPick(items) || '';

// 正確
path = await vscode.window.showQuickPick(items) ?? '';

eslint@typescript-eslint/no-inferrable-types

// 錯誤, Type string trivially inferred from a string literal, remove type annotation.
consoleKernel: string = '';

// 正確
consoleKernel = '';

eslint@no-useless-escape

取消多餘的跳脫字元。

// 錯誤
const cmdFnPattern = /function commands\([^\)]*[^{]+([^}]+)/m;

// 正確
const cmdFnPattern = /function commands\([^)]*[^{]+([^}]+)/m;

eslint@prefer-const

不會變動的變數,建議使用 const 定義。

// 錯誤, 'commands' is never reassigned. Use 'const' instead.
let commands = new Map;

// 正確
const commands = new Map;

eslint@typescript-eslint/no-misused-promises

// 錯誤, Promise returned in function argument where a void return was expected.
const commandDispose = vscode.commands.registerTextEditorCommand('extension.vscode-laravel-goto', Command);

// 正確
const commandDispose = vscode.commands.registerTextEditorCommand('extension.vscode-laravel-goto', () => Command);

eslint@typescript-eslint/no-unsafe-argument

any 型別需要適當的轉型處理。

// 錯誤, Unsafe argument of type `any` assigned to a parameter of type `IOpenAllArgs`.
const newWindowDispose = vscode.commands.registerCommand(
	'extension.vscode-laravel-goto.new-window',
	(args) => newWindow(context, args)

// 正確
const newWindowDispose = vscode.commands.registerCommand(
	'extension.vscode-laravel-goto.new-window',
	(args) => newWindow(context, args as IOpenAllArgs)

eslint@typescript-eslint/no-empty-function

// 錯誤, Unexpected empty function 'deactivate'.
export function deactivate() {}

// 正確, 直接刪除未使用的方法

eslint@typescript-eslint/unbound-method

預先綁定,避免在方法裡頭調用 this 產生非預期的問題。

// 錯誤, Avoid referencing unbound methods which may cause unintentional scoping of `this`.
const places = [
  this.pathHelperPlace,
]

// 正確
const places = [
  this.pathHelperPlace.bind(this),
]

eslint@typescript-eslint/prefer-for-of

簡單的 for loop 會建議改用 for-of 取代。

// 錯誤, Expected a `for-of` loop instead of a `for` loop with this simple iteration
for (let i = 0; i < places.length; i++) {
	place = await places[i](this, place, this.document, this.selection);
	if (place.path) {
		return place;
	}
}

// 正確
for(const thePlace of places) {
	place = await thePlace(this, place, this.document, this.selection);
	if (place.path) {
		return place;
	}
}

eslint@no-extra-boolean-cast

避免多餘的 Boolean 型別轉換。

// 錯誤, Redundant Boolean call.
if ((Boolean)(match && match[3] === ctx.path))

// 正確
if ((match && match[3] === ctx.path))

eslint@typescript-eslint/no-for-in-array

避免 for-in 可能出現的潛在問題。

// 錯誤, For-in loops over arrays are forbidden. Use for-of or array.forEach instead.
for (const key in words) {

// 正確
words.forEach((word, key) => {

eslint@typescript-eslint/array-type

// 錯誤, Array type using 'Array<string>' is forbidden. Use 'string[]' instead.
let extensions: Array<string> = vscode.workspace.getConfiguration().get('laravelGoto.staticFileExtensions', []);

// 正確
let extensions: string[] = vscode.workspace.getConfiguration().get('laravelGoto.staticFileExtensions', []);

eslint@typescript-eslint/restrict-template-expressions

使用樣板表達式,要確保樣板裡頭內容是字串或數字。

// 錯誤, Invalid type "Uri" of template literal expression.
const commentCommandUri = vscode.Uri.parse(`command:${command}?${args}`);
return `[${path}](${commentCommandUri})`;

// 正確
const commentCommandUri = vscode.Uri.parse(`command:${command}?${args}`);
return `[${path}](${commentCommandUri.toString()})`;

eslint@no-extra-semi

// 錯誤, Unnecessary semicolon.
const files = await workspace.findFiles('**/resources/lang/**', 1);;

// 正確
const files = await workspace.findFiles('**/resources/lang/**', 1);

eslint@typescript-eslint/no-unsafe-assignment

// 錯誤, Unsafe assignment of an `any` value.
const routeRows: RouteRow[] = JSON.parse(raw.stdout);

// 正確
const routeRows: RouteRow[] = JSON.parse(raw.stdout) as RouteRow[];

eslint@typescript-eslint/no-explicit-any

// 錯誤, Unexpected any. Specify a different type.
export function spawnSync(command: string, args: string[]): SpawnSyncReturns<any>

// 正確
export function spawnSync(command: string, args: string[]): SpawnSyncReturns<Buffer>

eslint@no-async-promise-executor

// 錯誤, Promise executor functions should not be async.
return new Promise(async (c, e) => {

這條規則卡很久,因為裡頭有一個需要使用 await 的呼叫,但規則說明裡頭不應該有:

const testCase = await getTestCase(filePattern, +lineNumber);
if (testCase) {
	mocha.grep(testCase);
}

接著我把它改成:

getTestCase(filePattern, +lineNumber)
.then((testCase) => testCase && mocha.grep(testCase))
.catch(err => e(err)); // 如果不做 .catch 會違反另一個規則 eslint@typescript-eslint/no-floating-promises

最後總算可以移除 async

// 正確
return new Promise((c, e) => {

後話

添加三個擴充規則之後,錯誤其實蠻多的。原本還打算只添加一兩個擴充規則就好,但根據一個規則修改之後,又後續挑戰另外一個規則。最終才完成所有規則的修正。也多虧這些規則,讓我學到 TypeScript 應該注意的部分,包括一些型別如何適當轉換,以及 Promise 應用的相關知識。