Аннотирование JavaScript для вопросов Closure Compiler

У меня есть следующий typedef в файле externs:

/** @typedef ({eventNameArray: Array.<string>,eventArrayIndex: number}) */
var triggerNextData;

Хотелось бы, чтобы он использовался в качестве переданного параметра для функции triggerNext (которая вызовет следующее событие). И eventNameArray (массив строк), и eventArrayIndex являются обязательными.

Вот функция triggerNext, которая ожидает этот тип:

/** 
* @type {function(triggerNextData)}
*/
triggerNext: function(data){
...
}

Когда я называю это так:

mediator.triggerNext("hi there");

Я получаю предупреждение, как и ожидалось, но в предупреждении говорится, что eventNameArray не является обязательным:

найдено: требуется строка: {eventArrayIndex: число, eventNameArray: (Array.|null)} mediator.triggerNext ("привет");

Почему-то он не понимает, ему нужен массив строк (массив типа не показан), а массив необязателен.

Следующее не выдает никаких предупреждений:

mediator.triggerNext({eventNameArray:null,eventArrayIndex:0});
mediator.triggerNext({eventNameArray:[22,33],eventArrayIndex:0});

Я хотел бы знать, как ввести eventNameArray как обязательный массив строк, можно ли это сделать? Если да, то как бы я это сделал?


person HMR    schedule 12.03.2013    source источник
comment
пару вещей, используйте объект на основе конструктора вместо typedef (closuretools.blogspot.co.uk/2012/02/type-checking-tips.html), также требуется аннотация !.   -  person lennel    schedule 12.03.2013
comment
Благодарим за ваше предложение. Жаль, что blogspot заблокирован здесь, в Китае, поэтому я не могу это прочитать. Я вижу много статей об этом в blogspot, но не вижу их. Однако triggerNext никогда не создается с новым triggerNext. Это свойство объекта, например var mediator={triggerNext:function(...   -  person HMR    schedule 12.03.2013
comment
собираюсь вставить всю статью для вас в ответ.   -  person lennel    schedule 12.03.2013


Ответы (2)


скопировано дословно с closuretools.blogspot.co.uk/2012/02/type-checking-tips.html из-за «Спасибо за ваше предложение. Жаль, что blogspot заблокирован здесь, в Китае, поэтому я не могу это прочитать. Я вижу много статей об этом есть на blogspot, но я их не вижу"

Язык типов Closure Compiler немного сложен. В нем есть объединения («переменная x может быть A или B»), структурные функции («переменная x — функция, возвращающая число») и типы записей («переменная x — любой объект со свойствами foo и bar»).

Многие люди говорили нам, что это все еще недостаточно выразительно. Есть много способов написать JavaScript, которые не вписываются в нашу систему типов. Люди предложили нам добавить примеси, трейты и постфактум именование анонимных объектов.

Нас это не особенно удивило. Правила объектов в JavaScript немного похожи на правила Calvinball. Вы можете изменить что угодно и придумать новые правила по ходу дела. Многие думают, что хорошая система типов дает вам мощный способ описать структуру вашей программы. Но это также дает вам набор правил. Система типов гарантирует, что все согласны с тем, что такое «класс», что такое «интерфейс» и что означает «есть». Когда вы пытаетесь добавить аннотации типов к нетипизированному JS, вы неизбежно столкнетесь с проблемами, когда правила в моей голове не совсем совпадают с правилами в вашей голове. Это нормально.

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

Функция против функции()

Есть два способа описать функцию. Один из них — использовать тип {Function}, который компилятор буквально интерпретирует как «любой объект x, где ‘x instanceof Function’ имеет значение true». {Function} намеренно мягкий. Он может принимать любые аргументы и возвращать что угодно. Вы даже можете использовать «новый» на нем. Компилятор позволит вам называть его так, как вы хотите, не выдавая предупреждений.

Структурная функция гораздо более специфична и дает вам более детальный контроль над тем, что может делать функция. {function()} не принимает аргументов, но нам все равно, что она возвращает. {function(?): number} возвращает число и принимает ровно один аргумент, но нас не волнует тип этого аргумента. {function(new:Array)} создает массив, когда вы вызываете его с помощью «нового». В нашей документации по типам и руководстве по стилю JavaScript есть больше примеров использования структурных функций.

Многие люди спрашивали нас, не рекомендуется ли использовать {Function}, потому что он менее конкретен. На самом деле, это очень полезно. Например, рассмотрим определение Function.prototype.bind. Он позволяет каррировать функции: вы можете дать ему функцию и список аргументов, и он вернет вам новую функцию с «предварительно заполненными» аргументами. Наша система типов не может выразить, что возвращаемый тип функции является преобразованием типа первого аргумента. Таким образом, JSDoc на Function.prototype.bind говорит, что он возвращает {Function}, и компилятор должен иметь логику, закодированную вручную, чтобы определить реальный тип.

Также во многих случаях вы хотите передать функцию обратного вызова для сбора результатов, но результаты зависят от контекста.

rpc.get(‘MyObject’, function(x) {
  // process MyObject
});

Метод «rpc.get» гораздо более неуклюж, если аргумент обратного вызова, который вы передаете, должен привести к типу все, что он получает. Так что часто бывает проще просто присвоить параметру тип {Function} и быть уверенным, что тип вызывающего объекта не стоит проверять.

Объект против анонимных объектов

Многие библиотеки JS определяют один глобальный объект с множеством методов. Аннотацию какого типа должен иметь этот объект?

var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};

Если вы пришли из Java, у вас может возникнуть соблазн просто указать тип {Object}.

/** @type {Object} */ var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};

Обычно это не то, что вы хотите. Если вы добавите аннотацию «@type {Object}», вы не просто скажете компилятору, что «bucket — это объект». Вы говорите ему, что «ведро может быть любым объектом». Таким образом, компилятор должен предположить, что любой может присвоить «корзине» любой объект, и программа по-прежнему будет типобезопасна.

Вместо этого вам часто лучше использовать @const.

/** @const */ var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};

Теперь мы знаем, что ведро нельзя присвоить какому-либо другому объекту, а механизм вывода типов компилятора может выполнять гораздо более строгие проверки ведра и его методов.

Может ли все быть просто типом записи?

Система типов JavaScript не так уж сложна. Он имеет 8 типов со специальным синтаксисом: null, undefined, boolean, число, строка, объект, массив и функция. Некоторые люди заметили, что типы записей позволяют определить «объект со свойствами x, y и z», а определения типов позволяют дать имя любому выражению типа. Таким образом, между ними вы должны иметь возможность определить любой пользовательский тип с типами записей и определениями типов. Это все, что нам нужно?

Типы записей хороши, когда вам нужна функция, принимающая большое количество необязательных параметров. Итак, если у вас есть эта функция:

/**
 * @param {boolean=} withKetchup
 * @param {boolean=} withLettuce
 * @param {boolean=} withOnions
 */
function makeBurger(withKetchup, withLettuce, withOnions) {}

вы можете немного упростить вызов следующим образом:

/**
 * @param {{withKetchup: (boolean|undefined),
            withLettuce: (boolean|undefined),
            withOnions: (boolean|undefined)}=} options
 */
function makeBurger(options) {}

Это хорошо работает. Но когда вы используете один и тот же тип записи во многих местах программы, все может стать немного запутанным. Предположим, вы создали тип для параметра makeBurger:

/** @typedef  {{withKetchup: (boolean|undefined),
                withLettuce: (boolean|undefined),
                withOnions: (boolean|undefined)}=} */
var BurgerToppings;

/** @const */
var bobsBurgerToppings = {withKetchup: true};

function makeBurgerForBob() {
  return makeBurger(bobsBurgerToppings);
}

Позже Алиса создает ресторанное приложение поверх библиотеки Боба. В отдельном файле она пытается добавить лук, но лажает API.

bobsBurgerToppings.withOnions = 3;

Компилятор Closure заметит, что bobsBurgerToppings больше не соответствует типу записи BurgerToppings. Но он не будет жаловаться на код Алисы. Он будет жаловаться, что код Боба делает ошибку типа. Для нетривиальных программ Бобу может быть очень трудно понять, почему типы больше не совпадают.

Хорошая система типов не просто выражает контракты о типах. Это также дает нам хороший способ возложить вину, когда код нарушает контракт. Поскольку класс обычно определяется в одном месте, компилятор может выяснить, кто несет ответственность за нарушение определения класса. Но когда у вас есть анонимный объект, который передается множеству различных функций и свойства которого задаются из разных мест, гораздо сложнее — как для людей, так и для компиляторов — выяснить, кто нарушает контракты типов.

Автор: Ник Сантос, инженер-программист

person lennel    schedule 12.03.2013
comment
Спасибо. Добавили! посмотрим, смогу ли я заставить его предупреждать лучше. Приложение не такое уж большое, поэтому оно не должно иметь большого значения, но мне было любопытно, как его использовать. - person HMR; 12.03.2013

Все объекты могут быть null по умолчанию:

Все типы объектов по умолчанию допускают значение NULL независимо от того, объявлены они с помощью оператора Nullable или нет.

Вы можете использовать !, чтобы сказать, что это должно быть ненулевое значение:

@typedef ({eventNameArray:!Array.<string>, eventArrayIndex:number})

Что касается того, чтобы элементы массива были строками, я не знаю (пока).

person Felix Kling    schedule 12.03.2013
comment
Спасибо, lennel также предложил использовать !. Надо еще проверить, но думаю исправят. Я не думаю, что закрытие по какой-то причине проверяет типы массивов, поскольку его можно указать, но, похоже, не выдает предупреждение, когда используются неправильно типизированные элементы массива. - person HMR; 12.03.2013
comment
вы получаете ошибки в массивах при использовании метода push, и вы должны это делать при переходе к конструктору класса, а не к typedef, как указано в статье, typedefs немного свободны и не должны использоваться для сопоставления сложных типов. - person lennel; 12.03.2013
comment
Не все типы объектов по умолчанию допускают значение NULL. Типы, созданные с использованием синтаксиса типа Record, по умолчанию не могут принимать значения NULL. См. эту страницу документации под заголовком «Записи»: github .com/google/close-compiler/wiki/ - person jamess; 26.07.2019