Comments 31
При реализации интерфейсов, и работе с объектами корректность соблюдения принципа подстановки Барбары Лисков ложится на плечи разработчиков.
Просто надо по возможности использовать "стрелочную" форму, тогда всё будет окей.
interface IA {
setValue: (a: number | string) => void;
}
Спасибо. Актуальное замечание. Добавил в заметку.
Ага, спасибо. На всякий допишу тут ещё 2 поинта (если вдруг у читателей возникнут вопросы). Во первых, для типов/интерфейсов нет разницы в поведении между стрелочной и "обычной" формой, как для объектов js (значения this, super, и т.д.), и реализовывать в классе можно и так и сяк, что видно в примере. Во вторых, если нужны перегрузки, то, например, аналогом такого для "обычной" формы
interface IA {
setValue(a: number | string): void;
setValue(a: boolean): boolean;
}
будет такой код
interface IA {
setValue: {
(a: number | string): void;
(a: boolean): boolean;
};
}
interface IA { setValue: (a: number | string) => void;}
Попробовал использовать и что-то не пошло.
Если имплементировать такой интерфейс, то тс будет требовать что бы setValue было свойством, а не методом. А тут просто ворох неудобств. И может быть правило требующие инициировать данное свойство в конструкторе. И в конечном итоге setValue становится частью экземпляра, а не прототипа. Вы сталкивались с подобным?
Не понял, в чем затруднение, у меня всё норм. работает
Уже не могу вспомнить контекста.
В одном из проектов, когда я попытался провернуть такую штуку от меня линтер начал требовать, что бы я инициализировал setValue в конструкторе, т.к. это свойство. Ну, или использовал расширенный синтаксис.
Сейчас уже не могу вспомнить где с этим столкнулся. Сам только что проверил и strict этому никак не препятствует. Я поэтому из статьи и не стал это убирать.
Я сейчас посмотрел, если интерфейсы задавать через абстрактные классы как раз и получается:
TS2425: Class <AbstractClassName> defines instance member property <Method>, but extended class <ExtendedClassName> defines it as instance member function.
запись метода как стрелочная функция так как метод становится свойством this а не прототипа, поэтому это абсолютно разная семантика. typescript достаточно коряво и неполностью поддерживает классы, работать с классами больно
Имелось в виду решение добавленное в заметку, т.е. речь идет об определении интерфейса. А так как интерфейс является по определению набором публичных свойств и методов, то делать какие-то предположения о this по интерфейсу странно.
Чего конкретно тайпскрипт не поддерживает в классах? Мне приходит в голову только return в конструкторе, которым в принципе крайне редко пользуются даже в жс.
Во "Вариант 2" вы явно забыли комменты с результатами // true | false
:
type E3 = TC extends TD ? true : false
type E4 = TD extends TC ? true : false
type TC = (a: number | string) => void
type TD = (a: string) => void
мне кажется впихивать 2 типа в 1 не самый лучший вариант, ведь всегда можно сделать перегрузку разве нет?
type TC = (a: number) => void;
type TD = (a: string) => void;
const foo: TC & TD = (a: string|number) => {
if(typeof a === 'string') {
// logic
} else {}
}
Перегрузка полезна, когда у нас есть связь между параметрами и выходным значением. Когда такой связи нет в перегрузке тоже никакого смысла нет.
А второй момент, где в вашем примере перегрузка?
К тому же, данные примеры можно контролировать на ts используя соответствующие настройки типа strict.

[занудамод]Тут у меня придирка конкретно к примеру - зачем делать так, если нужно делать так. Если пример только для примера - то всегда будет такое. Советую пытаться находить "рабочий" код для таких вещей- так как если в начале возникают сомнения, то дальше быть солидарным с автором становиться всё сложнее и сложнее :) [/занудамод]
А так делать не нужно. Выше я указал, когда нужна перегрузка.
Обратите внимание, у нас функция возвращает void. Никакого смысла ее перегружать нет. Например, если бы она у нас всегда возвращала string ее тоже не нужно было бы перегружать. Поэтому мой пример совершенно валидный и спокойно проецируется на любой продакшен.
То, что вы привели - это антишаблон, потому что у вас типы просто ради типов. Нельзя так делать.
И мне будет любопытно узнать, как ваш подход можно использовать для настоящей перегрузки, типа когда `(a: number) => number` и `(a: string) => string`
Мне интересно с каких пор перегрузка параметров является антипатерном? Может быть если я напишу вот так - то всё будет нормально?
class Point {
constructor(private x: number, private y: number) {}
move(d: string): void
move(d: number): void
move(d: string | number) {
this.x += +d || 0;
this.y += +d || 0;
}
}
const a = new Point(1,1);
a.move('1')
Тогда же опять проблема в примере - и это то что по моему мнение не очень удачно подобрано.
ну а последнее вообще странно
(a: number) => number
(a: string) => string
так как любой маломальский знающий TS напишет
<T>(a: T) => T
Мы тут ТС обсуждаем и на нем объединения, насколько я помню, были всегда, поэтому использовать перегрузку вместо объединения просто потому что там объединение - это антишаблон. Повторяю, если выход никак не зависит от входа, то использовать перегрузку на ТС не имеет никакого смысла.
Поэтому у меня с примерами в этом плане все нормально. А вы разными способами используете анти шаблон. То, что вы сейчас написали - это тоже анти шаблон.
По поводу дженериков, все верно. Очень часто, когда знакомят с дженериками показывают их работу как естественную замену перегрузки функции. Кстати, и у дженериков есть абсолютно такое же правило, что должна быть связь между данными. Там так же <T>(a: T): void является анти шаблоном.
Можете для ясности number/string заменить на какие-нибудь уникальные интерфейсы и тогда у нас будет не функция идентичности, а какая-то фабрика.
Можно пожалуйста ссылку на источник где сказано что это антишаблоны, ни капли не верится
Во первых, вы логически подумайте какой смысл плодить код в котором на TS нет никакого смысла? Подобное и есть один из признаков анти шаблона. А ничего кроме лишнего кода в ваших примерах нет.
Во вторых, т.к. новички совершают такие ошибки довольно часто, есть официальная документация:
https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types
Ну так загляните в эту доку - буквально тоже самое что я написал, только не воид а Moment
возвращается :D
function fn(x: string): Moment;
function fn(x: number): Moment;
function fn(x: number | string) {
// When written with separate overloads, incorrectly an error
// When written with union types, correctly OK
return moment().utcOffset(x);
}
а смысл от этого огромный - так как типизация это одновременно документация:


Уже первый вариант более приятный, так как относится к конкретному юзкейсу. А если число типов которые могут быть переданы будет намного больше и это будут не примитивы, а какие-нибудь сложные классы, которые значительно влияют на исполнение(например возникает возможность выброса особых исключений) то первый вариант будет юзерфрендли, в отличии от второго. И материть будут за второй код, в то время как за первый будут хвалить :D
Ну так загляните в эту доку - буквально тоже самое что я написал, только не воид а
Moment
возвращается :D
В документации написано, что так как вы советуете делать не надо. На этом точка.
Если хотите продолжить настаивать на своем советую вам написать свою небольшую заметку в которой вы наглядно объясните почему в документации по ТС советуют неправильно.
Кстати, любопытства ради, а что вам мешает вместо кучи мусорного кода просто написать
/**
* @param {string} x maybe string value
* @param {number} x maybe number value
*/
Я смотрю в результирующей справке нормально все сливается. Зачем вы тогда столько мусора в код добавляете?
Я устал чтото пытаться обьяснить человеку - который не может адекватно прочитать не то что коментарий, а даже материал который скидывает... Сами попытайтесь понять почему выше указаное не работает, и советую избавиться от привычки вырезать как можно больше кода - это ведёт к очень плачевным результатам...
Так я же вам предложил выход. Разработчики ТС прямо сейчас с ваших слов ошибаются и толкают неправильные мысли. Причем в письменном виде. Напишите заметку как они ошибаются.
А заодно познакомите сообщество с тем как надо оформлять функции. А то открываешь такой lib.es5.dt.s, а там один за другим:
/**
* Replaces text in a string, using a regular expression or search string.
* @param searchValue A string or regular expression to search for.
* @param replaceValue A string containing the text to replace. When the {@linkcode searchValue} is a `RegExp`, all matches are replaced if the `g` flag is set (or only those matches at the beginning, if the `y` flag is also present). Otherwise, only the first match of {@linkcode searchValue} is replaced.
*/
replace(searchValue: string | RegExp, replaceValue: string): string;
/**
* Replaces text in a string, using a regular expression or search string.
* @param searchValue A string to search for.
* @param replacer A function that returns the replacement text.
*/
replace(searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string;
И ведь надо же, всем все понятно. А так, получается, делать нельзя. Мало места занимает. Нужно использовать липовые перегрузки функций. Кстати, обратите внимание, ваш бесполезный код для одной функции, из примера выше, занимает 10 строк, выразительная документация с лишней пустой строкой двух функций занимает 13 строк. Просто, еще одна информация к размышлению.
Странный какой-то комментарий. В статье говорилось о контравариантности параметров, и флаге strict, для того чтобы это нормально проверялось. Причем тут перегрузки?
Система типов и настройки