Type Queries (запросы типа), Alias (псевдонимы типа)

Как сначала определить сложное значение, а затем одной строкой описать его тип? Или как конкретизировать более общий идентификатор типа и тем самым увеличить семантическую привлекательность кода? На два эти важные вопроса и поможет ответить текущая глава.

Запросы Типа (Type Queries)

Механизм запроса типа позволяют получить тип связанный со значением по его идентификатору и в дальнейшим использовать его как обычный тип. Запрос типа осуществляется оператором typeof после которого идет идентификатор ссылающийся на значение. Запрос типа также может располагается в местах указания типа.

ts
let v1: T1;
let v2: typeof v1; // let v2: T1;

С помощью данного механизма можно получить тип любой конструкции будь то переменная, параметр функции или метода, а также членов объекта и класса.

ts
class T {
    static staticProp: number;
    
    field: string;
    
    get prop(): boolean {
        return true;
    }
    
    method(): void {
    
    }
}

let t: T = new T();

let v0: typeof t; // let v0: T
let v1: typeof T.staticProp; // // let v1: number
let v2: typeof t.field; // let v2: string
let v3: typeof t.prop; // let v3: boolean
let v4: typeof t.method; // let v4: ()=>void

function f(param: number): void {
    let v: typeof param; // let v: number
}

Запрос типа может быть очень полезен сторонникам минимализма достигаемого при помощи вывода типов. К слову я один из них. Тем, кто придерживается консерватизма, возможно придется по душе идея ускорять написание тестов за счет механизма вывода типов. Ведь в тех ситуациях, когда для тестирования требуются не определенные в приложении типы данных, часто не хочется тратить время на их декларацию, но при этом хочется использовать автодополнение. Например при тестировании метода класса может понадобится тип представляющий только его, но поскольку для проекта подобный тип просто бессмыслен, его придется определять в контексте самих тестов, что гораздо проще сделать при помощи механизма запроса типа. Все это вместе в TypeScript становится возможным благодаря выводу типов в паре с оператором запроса типа.

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

ts
const STANDART_NORMAL = { x: 0, y: 0 }; // данные, которые нужны только для контролирования точности самих тестов. А это, в свою очередь, означает, что декларация типов для них ещё не определена. Хотя вывод типов в состоянии вывести тип {x: number, y: number} для этой константы.

// здесь вывод типа не в состоянии вывести тип параметров функции
function valid(standart) {
    let element = document.querySelector('#some-id');
    let { clientLeft: x, clientTop: y } = element;
    let position = { x, y };

    // поэтому о параметрах невозможно получить какую-либо информацию 
    let isPositionXValid = position.x === standart. // автодополнение отсутствует
    let isPositionYValid = position.y === standart. // автодополнение отсутствует
    
    // ...
}

Не стоит даже рассуждать - оставить так или указать типы, которые, возможно, предварительно нужно ещё задекларировать. Вместо этого нужно прибегнуть к механизму запроса типа. Запрос типа позволяет одновременно решить две задачи, одна из которых связана с проблемами сопутствующими типу any, а другая — минимализму и расходу времени на декларирование типов.

ts
const STANDART_NORMAL = { x: 0, y: 0 };

// получение типа для аннотирования параметров прямо из константы.
function valid(standart: typeof STANDART_NORMAL) {
    let element = document.querySelector('#some-id');
    let { clientLeft: x, clientTop: y } = element;
    let position = { x, y };
    
    // расходовать время на декларацию типа так и не пришлось. Тем не менее автодополнение работает.
    let isPositionXValid = position.x === standart.x; // выводит .x
    let isPositionYValid = position.y === standart.y; // выводит .y
    
    // ...
}

Псевдонимы Типов (Type Aliases)

Создание псевдонимов типа (types alias) — ещё одна из множества возможностей TypeScript которые невозможно переоценить. Псевдоним типа объявляется при помощи ключевого слова type, после которого следует идентификатор (имя) псевдонима, а за ним идет оператор присваивания =, справа от которого находится тип ассоциирующийся с псевдонимом.

ts
type Alias = T1;

Объявляться псевдоним типа может в контексте модулей, функций и методов.

ts
class Type {
    method(): void {
        type Alias = Type;
    }
}

type Alias = Type;

function func(): void {
    type Alias = Type;
}

Так как псевдонимы типов являются лишь псевдонимами для реальных типов, они не оставляют следа в коде после компиляции, к тому же их нельзя было расширять (extends) и реализовать (implements) в ранних версиях языка (до 2.7). Сейчас псевдоним типа можно реализовать или расширить, только если он представляет объектный тип (object type) или пересечение объектных типов со статически известными членами. Кроме того, псевдонимы типов нельзя использовать в таких операциях с типами времени выполнения как typeof и instanceof. Если псевдоним типа будет создан для объекта, то при попытке создать его экземпляр возникнет ошибка.

ts
class Class {
    f1: number;
    f2: string;
}

type ClassAlias = Class;

let v1: ClassAlias = new Class(); // Ok
let v2: ClassAlias = new ClassAlias(); // Error

Псевдонимы типов можно создавать как для типов объединений, так и для типов пересечений.

ts
type SomeType = number | string | boolean; // union type
type OtheType = number & string & boolean; // intersection type

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

ts
class BirdSignDataProvider {}
class FishSignDataProvider {}
class InsectSignDataProvider {}

function animalSignValidate(
    signProvider: BirdSignDataProvider | FishSignDataProvider | InsectSignDataProvider
): boolean {
  return true;
}

При работе с типами объединения и пересечения псевдонимы типов позволяют повысить читаемость кода за счет сокрытия множества типов за одним компактным идентификатором.

ts
class BirdSignDataProvider {};
class FishSignDataProvider {};
class InsectSignDataProvider {};

type AnimalSignProvider =
    BirdSignDataProvider |
    FishSignDataProvider |
    InsectSignDataProvider;

function animalSignValidate(signProvider: AnimalSignProvider): boolean {
    return true;
}

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

ts
// aliases.ts
import BirdSignDataProvider from './BirdSignDataProvider';
import FishSignDataProvider from './FishSignDataProvider';
import InsectSignDataProvider from './InsectSignDataProvider';

export type AnimalSignProvider =
    BirdSignDataProvider |
    FishSignDataProvider |
    InsectSignDataProvider;


// index.ts
import { AnimalSignProvider } from './aliases';

import BirdSignDataProvider from './BirdSignDataProvider';
import FishSignDataProvider from './FishSignDataProvider';
import InsectSignDataProvider from './InsectSignDataProvider';

function animalSignValidate(signProvider: AnimalSignProvider): boolean {
    return true;
}

animalSignValidate(new BirdSignDataProvider());
animalSignValidate(new FishSignDataProvider());
animalSignValidate(new InsectSignDataProvider());

Как было сказано ранее в главе “Псевдонимы Типов (Type Aliases)”, в тех редких случаях, когда декларированием типов, требующихся только для тестирования, можно пренебречь, механизм запроса типов помогает получить тип для указания в аннотации типа прямо из значения. Это дает все возможности типизации, за исключением читаемости кода, поскольку выражение запроса не персонализирует полученный тип. Хотя в примере, иллюстрирующим работу механизма запроса типа, константа STANDART_NORMAL имеет вполне говорящий идентификатор, допускаются случаи, при которых подобного будет сложно добиться. При худшем сценарии идентификатор может иметь общий смысл.

ts
let data = { x: 0, y: 0 };

function valid(standart: data) { // data, что это?

} 

В таких случаях псевдоним типа может оказать неоценимую помощь. Ведь экономия времени затраченного на декларирование типов не лишит код его выразительности и семантики.

ts
const STANDART_NORMAL = { x: 0, y: 0 };

type StandartNormalPoint = typeof STANDART_NORMAL; // определение "говорящего типа" без затраты времени на его декларирование.


function valid(standart: StandartNormalPoint) {
    // ...
    
    // Расходовать время на декларацию типа не пришлось, при этом работает автодополнение и параметр функции обзавелся типом, в чьем названии заключено его предназначение.
    let isPositionXValid = position.x === standart.x; // выводит .x
    let isPositionYValid = position.y === standart.y; // выводит .y
    
    // ...
}

Есть ещё пара особенностей псевдонимов, указание которых в данной главе было бы преждевременно, поэтому здесь о них будет лишь упомянуто. Во-первых, вынести объявления кортежа (Tuple), речь о котором пойдет далее в главе “Типы - Object, Array, Tuple”, можно только в описание псевдонима. Во-вторых, создать тип сопоставления как, например, Readonly, Partial, Pick, Record и им подобных, можно исключительно на основе псевдонимов типов. Перечисленные типы будут подробно рассмотрены в главе “Расширенные типы - Readonly, Partial, Required, Pick, Record”.