TypeScript — поистине прекрасный язык. В его арсенале есть все, что необходимо для качественной разработки. И если вдруг, кому-то знакомы секс-драматические этюды с JavaScript, то меня поймет. TypeScript имеет ряд допущений, неожиданный синтаксис, восхитительные конструкции, которые подчеркивают его красоту, форму и наполняют новым смыслом. Сегодня речь о них, об этих допущениях, о магии выражений. Кому интересно, добро пожаловать.
Правда N1.
Большинство конструкций и неожиданных находок, изложенных ниже, мне впервые попались на глаза на страницах Stack Overflow, github или вовсе были изобретены самостоятельно. И только потом дошло — это все есть здесь или здесь. Поэтому заранее прошу отнестись с пониманием, если изложенные находки Вам покажутся банальными.
Правда N2.
Практическая ценность некоторых конструкций равна 0.
Правда N3.
Примеры проверялись под tsc версии 3.4.5 и целевой es5. На всякий случай под спойлером конфиг
Находка: в секции implements можно указывать интерфейсы, типы и классы. Нас интересуют последние. Подробности здесь
Думаю разработчики TypeScript позаботились о 'строгих контрактах', исполненных через ключевое слово class. Причем классы необязательно должны быть абстрактными.
Находка: в секции extends допускаются выражения. Подробности. Если задаться вопросом — можно ли унаследоваться от 2х классов, то формальный ответ — нет. Но если имеется в виду экспорт функциональности — да.
Находка: глубокий и в тоже время бессмысленный аноним.
А кто больше напишет слово класс с 4 extends в примере выше?
А еще больше?
Ну Вы поняли — просто класс!
Если Вы не используете настройки компиляции strictNullChecks и strictPropertyInitialization,
то скорее всего знания о восклицательном знаке прошли рядом с Вами… Помимо основного предназначения, для него отведены еще 2 роли.
Находка: Восклицательный знак в роли Non-null assertion operator
Этот оператор позволяет обращаться к полю структуры, которое может быть null без проверки на null. Пример с пояснением:
Итого, этот оператор позволяет убрать лишние проверки на null в коде, если мы уверены…
Находка: Восклицательный знак в роли Definite assignment assertion modifier
Этот модификатор позволит нам проинициализировать свойство класса потом, где-то в коде, при включенной опции компиляции strictPropertyInitialization. Пример с пояснением:
Но вс�� эта мини выкладка про восклицательный знак не имела бы смысла без минутки юмора.
Вопрос: Как вы думаете, скомпилируется ли следующее выражение?
Каждый, кто пишет сложные типы открывает для себя много интересного. Вот и мне повезло.
Находка: на подтип можно ссылаться по имени поля основного типа.
Когда Вы пишите типы сами, такой подход объявления навряд ли имеет смысл. Но бывает так, что тип приходит из внешней библиотеки, а подтип — нет.
Трюк с подтипом можно применять и для повышения читаемости кода. Представьте, что у Вас есть базовый класс с generic типом, от которого наследуются классы. Пример ниже иллюстрирует сказанное
Находка: с помощью системы типов TypeScript возможно добиться комбинаторного набора порожденных типов с покрытием нужной функциональности. Сложно сказано, знаю. Долго думал над этой формулировкой. Покажу на примере шаблона Builder, как о��ного из самых известных. Представьте, что Вам нужно построить некий объект используя этот шаблон проектирования.
Пока не смотрите на избыточный метод create, приватный конструктор и вообще на использование этого шаблона в ts. Сосредоточиться нужно на цепочке вызовов. Идея в том, что вызываемые методы должны быть использованы строго 1 раз. Причем Ваша IDE также должна знать об этом. Другими словами после вызова любого метода у экземпляра builder этот метод должен исключаться из списка доступных. Достичь такой функциональности нам поможет тип NarrowCallside.
Находка: с помощью системы типов TypeScript можно управлять последовательностью вызовов, указывая строгий порядок. В примере ниже с помощью типа DirectCallside продемонстрируем это.
Это все мои интересные находки по TypeScript на сегодня. Всем спасибо за внимание и до новых встреч.
Немного правды
Правда N1.
Большинство конструкций и неожиданных находок, изложенных ниже, мне впервые попались на глаза на страницах Stack Overflow, github или вовсе были изобретены самостоятельно. И только потом дошло — это все есть здесь или здесь. Поэтому заранее прошу отнестись с пониманием, если изложенные находки Вам покажутся банальными.
Правда N2.
Практическая ценность некоторых конструкций равна 0.
Правда N3.
Примеры проверялись под tsc версии 3.4.5 и целевой es5. На всякий случай под спойлером конфиг
tsconfig.json
{
«compilerOptions»: {
«outFile»: "./target/result.js",
«module»: «amd»,
«target»: «es5»,
«declaration»: true,
«noImplicitAny»: true,
«noImplicitReturns»: true,
«strictNullChecks»: true,
«strictPropertyInitialization»: true,
«experimentalDecorators»: true,
«emitDecoratorMetadata»: true,
«preserveConstEnums»: true,
«noResolve»: true,
«sourceMap»: true,
«inlineSources»: true
},
«include»: [
"./src"
]
}
«compilerOptions»: {
«outFile»: "./target/result.js",
«module»: «amd»,
«target»: «es5»,
«declaration»: true,
«noImplicitAny»: true,
«noImplicitReturns»: true,
«strictNullChecks»: true,
«strictPropertyInitialization»: true,
«experimentalDecorators»: true,
«emitDecoratorMetadata»: true,
«preserveConstEnums»: true,
«noResolve»: true,
«sourceMap»: true,
«inlineSources»: true
},
«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 на сегодня. Всем спасибо за внимание и до новых встреч.
