TypeScript — поистине прекрасный язык. В его арсенале есть все, что необходимо для качественной разработки. И если вдруг, кому-то знакомы секс-драматические этюды с JavaScript, то меня поймет. 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"
]
}


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


Находка: в секции 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 на сегодня. Всем спасибо за внимание и до новых встреч.