Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В жизни каждого разработчика на TypeScript наступает момент, когда ему хочется рвать все связи с типом any. А ведь по началу any казался таким милым! Сделай переменной аннотацию типа any и используй любое свойство и метод этой переменной так, как привык работать в JavaScript. Никаких тебе ошибок, все чинно и спокойно, по-старому.
Документация TypeScript оправдывает использование any только на время переноса кодовой базы из JavaScript в TypeScript, но считает постыдным его использование в полноценном проекте. Казалось бы, все хорошо, только в описании типов библиотечных функций самого TypeScript аннотации any встречаются. Очень полезный `JSON.parse`, один из таких примеров.
// из lib.es5.d.ts
interface JSON {
parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string;
stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string;
}
Выйти из положения позволяет умение TypeScript объединять описания интерфейсов. Давайте разберемся как это можно исправить библиотечное описание интерфейса JSON.
Использование слияния интерфейсов и перегрузки методов
interface JSON {
parse(
text: string,
reviver?: (this: unknown, key: string, value: unknown) => unknown,
): unknown;
}
Песочница
Слияние деклараций интерфейсов
Когда TypeScript компилятор встречает декларацию интерфейса с одним и тем же именем несколько раз, он не расстраивается, а использует все описания. TypeScript как бы логически складывает все, что известно об интерфейсе, в единое описание и в дальнейшем исходит из этого.
В простейшем случае описания просто добавляются. Следующий пример показывает счастливый TypeScript.
interface User {
name: string;
}
interface User {
memberSince: number;
}
const user: User = {
memberSince: 2022,
name: 'author'
}
Песочница
В первом варианте описания интерфейс User содержит одно поле `name`, во втором варианте тоже одно поле, но с другим именем. TypeScript объединил оба описания в одно. Все ожидаемо.
Во втором варианте описания интерфейса можно повторить описание поля из первого. Но если так, то повторение должно быть полным. Совпадать должны и имена и типы полей.
interface User {
name: string;
memberSince: number;
}
interface User {
memberSince: number;
}
const user: User = {
memberSince: 2022,
name: 'author'
}
Песочница
Если бы имена полей были одинаковыми, но разных вариантах поля имели разные типы, то TypeScript высказал бы свое недовольство кодом ошибки (2717). Как это видно в следующей песочнице.
interface User {
name: string;
memberSince: string;// пока еще все хорошо
}
interface User {
memberSince: number;//ошибка
}
const user: User = {
memberSince: 2022, // ошибка
name: 'author'
}
Песочница
Как работает перегрузка функций в TypeScript
В javascript нет перегруженных функций, а в TypeScript есть. На самом деле в TypeScript в наличии только декларация перегрузки функций. Чтобы создать видимость перегруженных функций автору нужно создать все перегруженные декларации и одну реализацию, где вручную исследовать полученные аргументы.
function getYear():number;
function getYear(ticks: number): number;
function getYear(iso:string):number;
//скрытая от вызывающей стороны реализация
function getYear(arg?: undefined| string|number):number{
if(typeof arg === 'undefined'){
return 2022;
}
return new Date(arg).getFullYear();
}
Песочница
Перегрузка методов интерфейса.
Декларация интерфейсов не подразумевает реализацию. При описании перегруженных методов интерфейса требуется лишь определить их сигнатуры.
interface User{
login(token:string):void;
login(name: string, password: string):void;
login():void;
}
declare const user:User;
//вариант 1
user.login("--secret--");
//вариант 2
user.login("admin","pa$5w||d");
//вариант 3
user.login();
Песочница
Мы обратим дополнительное внимание на то, что имена методов повторяются. Более того, при слиянии интерфейсов у TypeScript нет требования уникальности имени метода.
Этим мы и воспользуемся для решения вопроса с улучшенной типизацией метода parse
интерфейса JSON
Слияние с библиотечным интерфейсом
Чтобы понять, как работает следующий отрывок, мы вспомним, что при слиянии интерфейсов более поздние описания перегруженных функций используются раньше, чем оригинальные. А поскольку библиотечное описание встретится компилятору раньше любого нашего, то наше определение будет иметь приоритет. В чем вы можете убедиться в песочнице
interface JSON {
parse(
text: string,
reviver?: (this: unknown, key: string, value: unknown) => unknown,
): unknown;
}
const x = JSON.parse("0", function ():unknown {
console.log(this);// TypeScript подсказывает, что this: unknown
return 42;
});
console.log(x) // TypeScript подсказывает, что x:unknown
Теперь мы можем не опасаться что TypeScript перестанет нам помогать в борьбе за качественное ПО.