Хабрахабр

TypeScript. Магия выражений

TypeScript — поистине прекрасный язык. В его арсенале есть все, что необходимо для качественной разработки. И если вдруг, кому-то знакомы секс-драматические этюды с JavaScript, то меня поймет. TypeScript имеет ряд допущений, неожиданный синтаксис, восхитительные конструкции, которые подчеркивают его красоту, форму и наполняют новым смыслом. Сегодня речь о них, об этих допущениях, о магии выражений. Кому интересно, добро пожаловать.

Немного правды

Правда N1.

И только потом дошло — это все есть здесь или здесь. Большинство конструкций и неожиданных находок, изложенных ниже, мне впервые попались на глаза на страницах Stack Overflow, github или вовсе были изобретены самостоятельно. Поэтому заранее прошу отнестись с пониманием, если изложенные находки Вам покажутся банальными.

Правда N2.

Практическая ценность некоторых конструкций равна 0.

Правда N3.

4. Примеры проверялись под tsc версии 3. На всякий случай под спойлером конфиг 5 и целевой es5.

tsconfig.json

,
«include»: [
"./src"
]
}

Реализация и наследование

Находка: в секции implements можно указывать интерфейсы, типы и классы. Нас интересуют последние. Подробности здесь

abstract class ClassA { abstract getA(): string; } abstract class ClassB { abstract getB(): string; } // Да, tsc нормально на это реагирует abstract class ClassC implements ClassA, ClassB { // ^ обратите внимание, на использование implements с классами. abstract getA(): string; abstract getB(): string; }

Думаю разработчики TypeScript позаботились о 'строгих контрактах', исполненных через ключевое слово class. Причем классы необязательно должны быть абстрактными.

Подробности. Находка: в секции extends допускаются выражения. Но если имеется в виду экспорт функциональности — да. Если задаться вопросом — можно ли унаследоваться от 2х классов, то формальный ответ — нет.

class One { one = "__one__"; getOne(): string { return "one"; }
} class Two { two = "__two__"; getTwo(): string { return "two"; }
} // Теже миксины, но в удобном виде: Подсказки в IDE (кроме статических полей) и автокомплит как положено.
class BothTogether extends mix(One, Two) { // ^ находка в том, что в части extends допускаются выражения info(): string { return "BothTogether: " + this.getOne() + " and " + this.getTwo() + ", one: " + this.one + ", two: " + this.two; // ^ подсказки от IDE здесь и ^ имеется }
} type FaceType<T> = { [K in keyof T]: T[K];
}; type Constructor<T> = { // prototype: T & {[key: string]: any}; new(): T;
}; // TODO: эта реализация на коленке, можно не глядеть. Классная реализация есть на просторах интернета
function mix<O, T, Mix = O & T>(o: Constructor<O>, t: Constructor<T>): FaceType<Mix> & Constructor<Mix> { function MixinClass(...args: any) { o.apply(this, args); t.apply(this, args); } const ignoreNamesFilter = (name: string) => ["constructor"].indexOf(name) === -1; [o, t].forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).filter(ignoreNamesFilter).forEach(name => { MixinClass.prototype[name] = baseCtor.prototype[name]; }); }); return MixinClass as any;
} const bt = new BothTogether();
window.console.log(bt.info()); // >> BothTogether: one and two, one: __one__, two: __two__

Находка: глубокий и в тоже время бессмысленный аноним.

const Сlass = class extends class extends class extends class extends class {} {} {} {} {};

А кто больше напишет слово класс с 4 extends в примере выше?

Если так

// tslint:disable
const Class = class Class extends class Class extends class Class extends class Class extends class Class {} {} {} {} {};

А еще больше?

Вот так

// tslint:disable
const сlass = class Class<Class> extends class Class extends class Class extends class Class extends class Class {} {} {} {} {};

Ну Вы поняли — просто класс!

Восклицательный знак — безграничный оператор и модификатор

Если Вы не используете настройки компиляции strictNullChecks и strictPropertyInitialization,
то скорее всего знания о восклицательном знаке прошли рядом с Вами… Помимо основного предназначения, для него отведены еще 2 роли.

Находка: Восклицательный знак в роли Non-null assertion operator

Пример с пояснением: Этот оператор позволяет обращаться к полю структуры, которое может быть null без проверки на null.

// Для проверки включаем режим --strictNullChecks type OptType = { maybe?: { data: string; }; }; // ... function process(optType: OptType) { completeOptFields(optType); // Мы знаем наверняка, что метод completeOptFields заполнит все необязательные поля. window.console.log(optType.maybe!.data); // ^ - берем на себя ответственность, что здесь не null // если уберем !, то получим от tsc: Object is possibly 'undefined' } function completeOptFields(optType: OptType) { if (!optType.maybe) { optType.maybe = { data: "some default info" }; } }

Итого, этот оператор позволяет убрать лишние проверки на null в коде, если мы уверены…

Находка: Восклицательный знак в роли Definite assignment assertion modifier

Пример с пояснением: Этот модификатор позволит нам проинициализировать свойство класса потом, где-то в коде, при включенной опции компиляции strictPropertyInitialization.

// Для проверки включаем режим --strictPropertyInitialization
class Field { foo!: number; // ^ // Notice this '!' modifier. // This is the "definite assignment assertion" constructor() { this.initialize(); } initialize() { this.foo = 0; // ^ инициализация здесь }
}

Но вся эта мини выкладка про восклицательный знак не имела бы смысла без минутки юмора.

Вопрос: Как вы думаете, скомпилируется ли следующее выражение?

// Для проверки включаем режим --strictNullChecks
type OptType = { maybe?: { data: string; };
};
function process(optType: OptType) { if (!!!optType.maybe!!!) { window.console.log("Just for fun"); } window.console.log(optType.maybe!!!!.data);
}

Ответ

Да

Типы

Вот и мне повезло. Каждый, кто пишет сложные типы открывает для себя много интересного.

Находка: на подтип можно ссылаться по имени поля основного типа.

type Person = { id: string; name: string; address: { city: string; street: string; house: string; }
}; type Address = Person["address"];

Когда Вы пишите типы сами, такой подход объявления навряд ли имеет смысл. Но бывает так, что тип приходит из внешней библиотеки, а подтип — нет.

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

class BaseDialog<In, Out> { show(params: In): Out {/** базовый код. В конце return ... */ }
} // Декларация по-старинке
class PersonDialogOld extends BaseDialog<Person[], string> {/** код здесь */} // Повышаем читаемость
class PersonDialog extends BaseDialog<Person[], Person["id"]> {/** код здесь */}

Находка: с помощью системы типов TypeScript возможно добиться комбинаторного набора порожденных типов с покрытием нужной функциональности. Сложно сказано, знаю. Долго думал над этой формулировкой. Покажу на примере шаблона Builder, как одного из самых известных. Представьте, что Вам нужно построить некий объект используя этот шаблон проектирования.

class SimpleBuilder { private constructor() {} static create(): SimpleBuilder { return new SimpleBuilder(); } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; }
} const builder = SimpleBuilder.create();
// Так мы получаем требуемый объект.
const result = builder.firstName("F").lastName("L").middleName("M").build();

Пока не смотрите на избыточный метод create, приватный конструктор и вообще на использование этого шаблона в ts. Сосредоточиться нужно на цепочке вызовов. Идея в том, что вызываемые методы должны быть использованы строго 1 раз. Причем Ваша IDE также должна знать об этом. Другими словами после вызова любого метода у экземпляра builder этот метод должен исключаться из списка доступных. Достичь такой функциональности нам поможет тип NarrowCallside.

type ExcludeMethod<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type NarrowCallside<T> = { [P in keyof T]: T[P] extends (...args: any) => T ? ReturnType<T[P]> extends T ? (...args: Parameters<T[P]>) => NarrowCallside<ExcludeMethod<T, P>> : T[P] : T[P];
}; class SimpleBuilder { private constructor() {} static create(): NarrowCallside<SimpleBuilder> { return new SimpleBuilder(); } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; }
} const builder = SimpleBuilder.create();
const result = builder.firstName("F")
// ^ - доступны все методы .lastName("L")
// ^ - здесь доступны lastName, middleName и build .middleName("M")
// ^ - здесь доступны middleName и build .build();
// ^ - здесь доступен только build

Находка: с помощью системы типов TypeScript можно управлять последовательностью вызовов, указывая строгий порядок. В примере ниже с помощью типа DirectCallside продемонстрируем это.

type FilterKeys<T> = ({[P in keyof T]: T[P] extends (...args: any) => any ? ReturnType<T[P]> extends never ? never : P : never })[keyof T];
type FilterMethods<T> = Pick<T, FilterKeys<T>>; type BaseDirectCallside<T, Direct extends any[]> = FilterMethods<{ [Key in keyof T]: T[Key] extends ((...args: any) => T) ? ((..._: Direct) => any) extends ((_: infer First, ..._1: infer Next) => any) ? First extends Key ? (...args: Parameters<T[Key]>) => BaseDirectCallside<T, Next> : never : never : T[Key]
}>; type DirectCallside<T, P extends Array<keyof T>> = BaseDirectCallside<T, P>; class StrongBuilder { private constructor() {} static create(): DirectCallside<StrongBuilder, ["firstName", "lastName", "middleName"]> { return new StrongBuilder() as any; } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; }
} const sBuilder = StrongBuilder.create();
const sResult = sBuilder.firstName("F")
// ^ - доступны только firstName и build .lastName("L")
// ^ - доступны только lastName и build .middleName("M")
// ^ - доступны только middleName и build .build();
// ^ - доступен только build

Итого

Всем спасибо за внимание и до новых встреч. Это все мои интересные находки по TypeScript на сегодня.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть