Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров36K
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 на сегодня. Всем спасибо за внимание и до новых встреч.
Теги:
Хабы:
+35
Комментарии23

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн