Страницы

воскресенье, 9 апреля 2017 г.

VS Code JavaScript Intellisense

Не смотря на то, что с TypeScript я познакомился еще до появления на свет Visual Studio Code, мне еще ни разу не довелось использовать его в повседневной работе. Пишу на ES6 (Node.js), стараюсь документировать код с помощью jsdoc, но зачастую одних только комментов не хватает для того, чтобы intellisense подсказал мне с чем я имею дело в текущий момент времени, а иногда очень хотелось бы (и не мне одному), поэтому я решил попробовать миксовать declaration files с "vanilla JS".


Сначала покажу что же получилось в итоге.

Модуль:
- node_modules/incrementer/index.js:
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
 
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
 
module.exports = (value, opts) => new Inc(value, opts);

Приложение:
- app.js:
const Inc = require('incrementer');
const inc = Inc(1, { prefix: 'result' });
const result = inc.inc(2);
console.log(result);

Подсказки:



"Ну после такого дождя жди хороший отёл" :).

А теперь по порядку. Семь подходов.

Подход #1. Ничего кроме кода.

Модуль:
- inc.js:
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
 
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
 
module.exports = (value, opts) => new Inc(value, opts);

Приложение:
- app.js:
const Inc = require('./inc');
const inc = Inc(1, { prefix: 'result' });
const result = inc.inc(2);
console.log(result);

Подсказки:

Intellisense "знает" имена параметров экспортируемой модулем функции, не более того.

Подход #2. Добавим комменты jsdoc.

Модуль:
- inc.js:
/**
 * @constructor
 * @param {number} value
 * @param {Object} opts
 * @param {string} opts.prefix
 */
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
/**
 * @param {number} value 
 * @param {string} [separator=': ']
 * @returns {string}
 */
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
/**
 * @param {number} value
 * @param {Object} opts
 * @param {string} opts.prefix
 */
module.exports = (value, opts) => new Inc(value, opts);

Приложение: без изменений.

Подсказки:

Уже кое-что: появились типы параметров.

Подход #3. Попробуем закомментировать подробнее.

Модуль:
- inc.js:
/**
 * @constructor
 * @param {number} value
 * @param {opts} opts
 */
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
/**
 * @typedef {function} inc
 * @param {number} value 
 * @param {string} [separator=': ']
 * @returns {string}
 */
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
/**
 * @typedef {Object} incrementer
 * @property {inc} inc
 */
 
/**
 * @typedef {Object} opts
 * @property {string} prefix
 */
 
/**
 * @param {number} value
 * @param {opts} opts
 * @returns {incrementer}
 */
module.exports = (value, opts) => new Inc(value, opts);

Приложение: без изменений.

Подсказки:


В настоящее время большая часть моего кода выглядит примерно так. И это максимум из того, что я могу выжать из jsdoc. Предложения принимаются.

Подход #4. Создадим файл объявления нашего модуля.

Модуль:
- inc.js:
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
 
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
 
/** @type {Inc} */
module.exports = (value, opts) => new Inc(value, opts);

Файл объявления:
typings/inc.d.ts:
interface Inc {
  /**
   * Returns incrementer
   */
  (value: number, opts: { prefix: string }): incrementer;
}
interface incrementer {
  /**
   * Increments number, returns string representation
   */
  inc(value: number, separator = ': '): string;
}

Приложение: без изменений.

Подсказки:



И это все что мне нужно от intellisense!

Подход #5. Используем классы.

Модуль: без изменений.

Файл объявления:
typings/inc.d.ts:
declare interface Inc {
  /**
   * Returns incrementer
   */
  (value: number, opts: { prefix: string }): CInc;
}
 
declare class CInc {
  inc: inc;
  constructor(value: number, opts: { prefix: string });
}
 
declare interface inc {
  /**
   * Increments number, returns string representation
   */
  (value: number, separator = ': '): string;
}

Можно было без префикса declare, но хуже не будет.

Приложение: без изменений.

Подсказки:



Тоже неплохо, наверное даже более... "честно" что ли, ведь на самом деле под капотом модуля живет конструктор, хотя и предыдущий вариант меня вполне устроил.

Подход #6. Создаем модуль в каталоге node_modules.

Модуль:
- node_modules/incr.js:
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
 
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
 
module.exports = (value, opts) => new Inc(value, opts);

Файл объявления:
typings/incr.d.ts:
declare module "incr" {
  /**
   * Returns incrementer
   */
  function Incr (value: number, opts: { prefix: string }): CIncr
 
  class CIncr {
    inc: incr;
    constructor(value: number, opts: { prefix: string });
  }
 
  interface incr {
    /**
     * Increments number, returns string representation
     */
    (value: number, separator = ': '): string;
  }
 
  export = Incr
}

Приложение:
- app.js:
const Inc = require('incr');
const inc = Inc(1, { prefix: 'result' });
const result = inc.inc(2);
console.log(result);

Подсказки:



Подход #7. Создаем еще один модуль в каталоге node_modules, включаем файл объявления в комплект поставки модуля.

Модуль:
node_modules/incrementer/index.js:
const Inc = function (value, { prefix }) {
  this._value = value;
  this._prefix = prefix;
};
 
const inc = function (value, separator = ': ') {
  return `${this._prefix}${separator}${value + this._value}`;
};
 
Inc.prototype = {
  inc
};
 
module.exports = (value, opts) => new Inc(value, opts);

- package.json:
{
  "name": "incrementer",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "types": "index.d.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "private": true
}

Обращаю внимание на поле "types": "index.d.ts" - значение по умолчанию, поэтому в нашем случае наличие этого поля не обязательно.

Файл объявления:
-  node_modules/incrementer/index.d.js:
declare module "incrementer" {
  /**
   * Returns incrementer
   */
  function Incrementer (value: number, opts: { prefix: string }): CIncrementer
 
  class CIncrementer {
    inc: increment;
    constructor(value: number, opts: { prefix: string });
  }
 
  interface increment {
    /**
     * Increments number, returns string representation
     */
    (value: number, separator = ': '): string;
  }
 
  export = Incrementer
}

Приложение:
app.js:
const Inc = require('incrementer');
const inc = Inc(1, { prefix: 'result' });
const result = inc.inc(2);
console.log(result);

Подсказки:



Вот и все, остается найти время написать файлы объявления к созданному ранее JavaScript коду, и "хороший отёл" не заставит себя долго ждать :).