Перейти к содержанию

Именная типизация

Система типов TypeScript является структурной и это одно из её главных преимуществ. Однако существуют реальные случаи использования системы, в которой необходимо различать две переменные, потому что они имеют разное имя типа, даже если они имеют одинаковую структуру. Очень распространенный вариант использования - это структуры identity (обычно это просто строки с семантикой, связанной с их именем в таких языках, как C#/Java).

В сообществе появилось несколько паттернов. Я расскажу о них в порядке убывания личного предпочтения:

Использование литеральных типов

В этом паттерне используются общие и литеральные типы:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** Общий Id тип */
type Id<T extends string> = {
    type: T;
    value: string;
};

/** Специальные Id типы */
type FooId = Id<'foo'>;
type BarId = Id<'bar'>;

/** Необязательно: функции-конструкторы */
const createFoo = (value: string): FooId => ({
    type: 'foo',
    value,
});
const createBar = (value: string): BarId => ({
    type: 'bar',
    value,
});

let foo = createFoo('sample');
let bar = createBar('sample');

foo = bar; // Ошибка
foo = foo; // Okay
  • Преимущества
    • Утверждения типа не требуются
  • Недостаток
    • Структура {тип, значение} может быть нежелательной и требовать поддержки серверной сериализации

Использование перечислений

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

Обходной путь предполагает:

  • Создание перечисления brand.
  • Создание типа как пересечение (&) перечисления brand + фактическая структура.

Это показано ниже, где структура типов представляет собой просто строку:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// FOO
enum FooIdBrand {}
type FooId = FooIdBrand & string;

// BAR
enum BarIdBrand {}
type BarId = BarIdBrand & string;

/**
 * Пример использования
 */
var fooId: FooId;
var barId: BarId;

// Предохранитель!
fooId = barId; // ошибка
barId = fooId; // ошибка

// Присвоение с утверждением
fooId = 'foo' as FooId;
barId = 'bar' as BarId;

// Оба типа совместимы с основой
var str: string;
str = fooId;
str = barId;

Использование интерфейсов

Поскольку числа совместимы по типу с перечислением, предыдущая техника для них не может быть использована. Вместо этого мы можем использовать интерфейсы, чтобы нарушить структурную совместимость. Этот метод все еще используется командой компилятора TypeScript, поэтому стоит его упомянуть. Использование префикса _ и суффикса Brand - это соглашение, которому мы рекомендуем следовать (и то, которому следует команда TypeScript).

Обходной путь включает в себя следующее:

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

Это показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// FOO
interface FooId extends String {
    _fooIdBrand: string; // Для предотвращения ошибок типа
}

// BAR
interface BarId extends String {
    _barIdBrand: string; // Для предотвращения ошибок типа
}

/**
 * Пример использования
 */
var fooId: FooId;
var barId: BarId;

// Предохранитель!
fooId = barId; // ошибка
barId = fooId; // ошибка
fooId = <FooId>barId; // ошибка
barId = <BarId>fooId; // ошибка

// Присвоение с утверждением
fooId = 'foo' as any;
barId = 'bar' as any;

// Если вам нужна базовая строка
var str: string;
str = fooId as any;
str = barId as any;

Комментарии