скопировано дословно с 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