Символы — один из примитивных типов в JavaScript, введенный в ECMAScript 2015 (ES6). В отличие от других примитивов (строк, чисел, булевых значений), главная особенность символов — их уникальность. Каждый символ, созданный с помощью конструктора Symbol(), гарантированно уникален:
| TypeScript | 1
2
3
| const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // всегда false, даже если конструкторы вызваны одинаково |
|
Эта фундаментальная особенность делает символы идеальным выбором для создания уникальных идентификаторов, которые никогда не будут конфликтовать. В JavaScript символы могут использоваться в качестве ключей объектов, создавая свойства, которые не могут быть случайно перезаписаны:
| TypeScript | 1
2
3
4
5
6
7
| const uniqueKey = Symbol('description');
const obj = {
[uniqueKey]: 'Секретное значение',
normalKey: 'Обычное значение'
};
console.log(obj[uniqueKey]); // 'Секретное значение' |
|
Обратите внимание на строку 'description', которую я передал в Symbol() — это необязательный параметр, который служит только для отладки и никак не влияет на уникальность символа. Два символа с одинаковым описанием всё равно считаются разными символами. Когда я начал работать с TypeScript, обнаружил, что система типов предоставляет дополнительные возможности для работы с символами. TypeScript различает общий тип symbol и более специфичный unique symbol, о котором мы поговорим в следующей главе.
Символы против строковых ключей
В чем же принципиальное преимущество символов перед обычными строками при использовании в качестве ключей? Я часто использую такую аналогию: представьте строки как имена, а символы как отпечатки пальцев. Два человека могут иметь одинаковое имя (конфликт ключей), но отпечатки пальцев всегда уникальны. Рассмотрим классический сценарий, с которым я столкнулся при разработке системы плагинов:
| TypeScript | 1
2
3
4
5
| // Плагин A
obj.extension = { filter: (data) => data.type === 'A' };
// Плагин B (перезаписывает расширение от плагина A)
obj.extension = { sort: (data) => data.sort((a, b) => a.name.localeCompare(b.name)) }; |
|
С символами проблема решается элегантно:
| TypeScript | 1
2
3
4
5
6
7
| // Плагин A
const extA = Symbol('extension-a');
obj[extA] = { filter: (data) => data.type === 'A' };
// Плагин B (использует свой собственный ключ, не конфликтует)
const extB = Symbol('extension-b');
obj[extB] = { sort: (data) => data.sort((a, b) => a.name.localeCompare(b.name)) }; |
|
В крупных проектах с множеством компонентов, особенно при использовании сторонних библиотек, символы помогают избежать конфликтов имён, делая код более предсказуемым и надежным.
Глобальные и локальные символы
В JavaScript существует два способа создания символов: локальные через Symbol() и глобальные через Symbol.for(). В этом контексте "глобальный" означает, что символ регистрируется в глобальном реестре символов.
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| // Локальный символ (всегда уникален)
const localSymbol = Symbol('myKey');
// Глобальный символ (доступен через реестр)
const globalSymbol = Symbol.for('myKey');
// Получение того же глобального символа
const sameGlobalSymbol = Symbol.for('myKey');
console.log(globalSymbol === sameGlobalSymbol); // true |
|
Когда вы вызываете Symbol.for('someKey'), JavaScript сначала проверяет, существует ли символ с таким ключом в глобальном реестре. Если да, возвращает его. Если нет, создает новый и регистрирует. Это особенно полезно, когда вам нужно использовать один и тот же символ в разных частях приложения или даже разных модулях.
В своей практике я часто использую глобальные символы для определения протоколов взаимодействия между модулями. Например, при создании системы плагинов, где каждый плагин должен реализовать определенный интерфейс:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // В ядре приложения
export const PLUGIN_INIT = Symbol.for('app.plugin.init');
export const PLUGIN_SHUTDOWN = Symbol.for('app.plugin.shutdown');
// В плагине
import { PLUGIN_INIT, PLUGIN_SHUTDOWN } from 'app-core';
class MyPlugin {
[PLUGIN_INIT]() {
console.log('Плагин инициализирован');
}
[PLUGIN_SHUTDOWN]() {
console.log('Плагин остановлен');
}
} |
|
Такой подход обеспечивает типобезопасное и надежное взаимодействие между компонентами, исключая возможность случайной перезаписи или неправильной реализации интерфейса.
Сравнение производительности символов и других примитивов
Когда я впервые начал активно использовать символы в своих проектах, меня стал мучать вопрос: "А нет ли здесь скрытых накладных расходов?". В конце концов, уникальность и специальные возможности должны как-то сказываться на производительности, верно? Провел несколько бенчмарков и был удивлен результатами. Оказывается, для большинства операций символы демонстрируют производительность, сопоставимую со строками. Вот что я выяснил:
1. Создание символов медленнее, чем создание строк, но это разовая операция.
2. Доступ к свойствам объекта по символьному ключу практически идентичен по скорости доступу по строковому ключу.
3. Сравнение символов === работает быстрее, чем сравнение строк, потому что не требует проверки содержимого.
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Бенчмарк, который я проводил
const stringKey = 'myKey';
const symbolKey = Symbol('myKey');
const obj = {
[stringKey]: 'string value',
[symbolKey]: 'symbol value'
};
console.time('string access');
for (let i = 0; i < 1000000; i++) {
const val = obj[stringKey];
}
console.timeEnd('string access');
console.time('symbol access');
for (let i = 0; i < 1000000; i++) {
const val = obj[symbolKey];
}
console.timeEnd('symbol access'); |
|
В реальных приложениях разница обычно незаметна, но в критических участках кода с интенсивными вычислениями символы могут даже дать небольшое преимущество, особенно при операциях сравнения.
Well-known символы
Помимо пользовательских символов, JavaScript предоставляет набор предопределенных "хорошо известных" (well-known) символов, доступных как свойства объекта Symbol. Они используются для взаимодействия с внутренними механизмами языка. Помню забавный случай из моей практики. Разрабатывал я как-то библиотеку для обработки данных, которая должна была поддерживать кастомную итерацию. Реализовал всё через обычные методы и столкнулся с тем, что мои объекты не работали в циклах for...of. После часа отладки я узнал о Symbol.iterator и всё сразу стало на места.
Вот некоторые из наиболее полезных well-known символов:
| 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
31
32
33
| // Делает объект итерируемым (for...of)
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const item of myIterable) {
console.log(item); // 1, 2, 3
}
// Определяет поведение при преобразовании в примитив
const myObject = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return 'Hello';
return true;
}
};
console.log(+myObject); // 42
console.log(`${myObject}`); // "Hello"
console.log(myObject + ''); // "true" |
|
Эти встроенные символы позволяют вам "перегружать" стандартное поведение объектов, адаптируя их под свои нужды. Но важно помнить, что это мощный инструмент, который следует использовать с осторожностью.
Контроль доступа и перечисление свойств
Одно из неочевидных преимуществ символов, которое я открыл для себя не сразу — они естественным образом создают "полуприватные" свойства объектов. Свойства с символьными ключами не отображаются при стандартном перечислении:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| const privateProperty = Symbol('private');
const obj = {
regular: 'Я видим всем',
[privateProperty]: 'Меня сложно найти'
};
console.log(Object.keys(obj)); // ['regular']
console.log(Object.getOwnPropertyNames(obj)); // ['regular']
// Только специальный метод видит символьные ключи
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(private)] |
|
Это не настоящая приватность в смысле строгого ограничения доступа, но очень удобный механизм для создания "внутреннего API" библиотек. Я часто использую этот подход, когда мне нужно хранить служебные данные в объектах, не засоряя их публичный интерфейс.
Если вспомнить проблемы, которые решают символы в JavaScript и TypeScript — уникальность, избежание конфликтов имен, полуприватное хранение данных — становится понятно, почему они были добавлены в язык. Я считаю их недооцененным инструментом, который значительно улучшает архитектуру сложных приложений.
Типизация символов в TypeScript

В мире TypeScript символы обретают дополнительный уровень глубины благодаря статической системе типов. Когда я впервые столкнулся с ними в проекте после перехода с JavaScript на TypeScript, то сразу заметил интересный нюанс — система типов TypeScript делит символы на две категории: обычные symbol и так называемые unique symbol. И эта разница оказалась критичной для построения надежных апи.
Типы symbol и unique symbol
В TypeScript есть два основных типа для работы с символами:
1. symbol — общий тип для всех символов.
2. unique symbol — специальный подтип, представляющий конкретный уникальный символ.
Разница между ними не очевидна, пока не начинаешь реально применять их в коде. Рассмотрим простой пример:
| TypeScript | 1
2
3
4
5
6
7
| // Переменные с let получают тип symbol
let sym1 = Symbol('sym1');
// TypeScript выводит тип: symbol
// Константы получают более конкретный тип
const sym2 = Symbol('sym2');
// TypeScript выводит тип: typeof sym2 (по сути, unique symbol) |
|
Тип symbol — это широкий тип, который говорит только "это какой-то символ", без конкретизации какой именно. А вот unique symbol (или по факту typeof конкретный_символ) — это тип, который представляет конкретный уникальный символ.
Лучше понял я эту разницу, когда попытался создать функцию, которая принимает только определенный символ:
| TypeScript | 1
2
3
4
5
6
7
8
| const AUTH_TOKEN = Symbol('auth');
function processAuth(token: typeof AUTH_TOKEN) {
// Работаем с токеном
}
processAuth(AUTH_TOKEN); // Работает
processAuth(Symbol('auth')); // Ошибка! Хотя описание то же самое |
|
TypeScript не дает нам использовать любой символ, даже с тем же описанием — он требует именно тот конкретный экземпляр, тип которого мы указали через typeof AUTH_TOKEN. Это мощный инструмент для обеспечения типобезопасности.
Явное использование unique symbol
Тип unique symbol можно использовать и напрямую, но с ограничениями. Он может применяться только в константах (const) или в статических свойствах классов с модификатором readonly:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| // Явное объявление типа unique symbol
const MY_SPECIAL_KEY: unique symbol = Symbol('special');
class ApiService {
static readonly API_VERSION: unique symbol = Symbol('version');
checkVersion(version: typeof ApiService.API_VERSION) {
// Гарантировано, что будет передан только правильный символ
}
} |
|
Попытка использовать unique symbol с let вызовет ошибку компиляции, что логично — переменная может быть перезаписана, нарушая гарантию уникальности.
Однажды я потратил несколько часов, пытаясь понять, почему TypeScript ругается на, казалось бы, правильный код:
| TypeScript | 1
2
3
4
| function createUniqueKey(): unique symbol {
// Ошибка! Значение типа 'symbol' не может быть присвоено типу 'unique symbol'
return Symbol('dynamic');
} |
|
Проблема в том, что unique symbol привязан к конкретному идентификатору во время компиляции, а функция создает символ динамически во время выполнения. Такое ограничение существует из-за природы системы типов TypeScript — она должна знать все типы на этапе компиляции.
Использование символов как ключей объектов
Самое интересное начинается, когда используешь символы как ключи объектов в TypeScript. Здесь обнаруживается масса нюансов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const METADATA = Symbol('metadata');
interface ConfigWithMetadata {
name: string;
[METADATA]: any; // Используем конкретный символ как ключ
}
const config: ConfigWithMetadata = {
name: 'Default Config',
[METADATA]: { version: '1.0.0' }
};
// Можно также определить индексные типы с символьными ключами
interface Cache {
[key: symbol]: unknown;
} |
|
Что мне нравится в TypeScript — он четко разделяет строковые и символьные ключи, и не позволяет случайно смешать их в индексных типах.
Потеря типа при присваивании
Одна из неочевидных проблем, с которой я регулярно сталкиваюсь — при присваивании символа новой переменной можно потерять уникальность его типа:
| TypeScript | 1
2
3
4
5
6
| const ORIGINAL = Symbol('original'); // typeof ORIGINAL (unique symbol)
const copy = ORIGINAL; // symbol
function acceptOriginal(s: typeof ORIGINAL) {}
acceptOriginal(ORIGINAL); // OK
acceptOriginal(copy); // Ошибка! Тип 'symbol' не присваиваемый к типу 'typeof ORIGINAL' |
|
Несмотря на то, что copy содержит тот же самый символ, что и ORIGINAL, TypeScript расценивает его как символ общего типа symbol. Это объясняется тем, что система типов следует принципу: если значение может меняться (не const), то мы не можем гарантировать его точный тип.
Это можно исправить явным указанием типа:
| TypeScript | 1
2
3
4
5
| const ORIGINAL = Symbol('original');
const copy: typeof ORIGINAL = ORIGINAL; // Теперь copy имеет тип typeof ORIGINAL
function acceptOriginal(s: typeof ORIGINAL) {}
acceptOriginal(copy); // Теперь работает |
|
Символы и дискриминированные объединения
Символы прекрасно работают в дискриминированных объединениях — паттерне, который часто используется в TypeScript для моделирования вариативных структур данных. Я активно использую этот подход в системах обработки событий:
| 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
| const CLICK_EVENT = Symbol('click');
const HOVER_EVENT = Symbol('hover');
type ClickEvent = {
type: typeof CLICK_EVENT;
x: number;
y: number;
};
type HoverEvent = {
type: typeof HOVER_EVENT;
elementId: string;
};
type UIEvent = ClickEvent | HoverEvent;
function handleEvent(event: UIEvent) {
switch (event.type) {
case CLICK_EVENT:
// TypeScript знает, что это ClickEvent
console.log(`Клик в координатах: ${event.x}, ${event.y}`);
break;
case HOVER_EVENT:
// TypeScript знает, что это HoverEvent
console.log(`Наведение на элемент: ${event.elementId}`);
break;
}
} |
|
Преимущество использования символов вместо строк здесь в том, что невозможно случайно передать неправильный тип события — символы гарантируют безопасность на уровне типов.
Объединения типов символов
Можно создавать объединения типов символов, что отлично подходит для моделирования вариативных API:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| const RED = Symbol('red');
const GREEN = Symbol('green');
const BLUE = Symbol('blue');
type PrimaryColor = typeof RED | typeof GREEN | typeof BLUE;
function setPrimaryColor(color: PrimaryColor) {
// Работаем с цветом, гарантированно получаем только разрешенные значения
}
setPrimaryColor(RED); // Работает
setPrimaryColor(Symbol('red')); // Ошибка! Не тот символ |
|
Такой подход обеспечивает типобезопасность на уровне, недоступном при использовании строковых литералов, где можно случайно опечататься или передать неправильное значение. В моей практике бывали случаи, когда символьные объединения спасали от серьезных ошибок в сложных системах обработки данных. Однажды, работая над системой плагинов, я использовал символы для идентификации различных точек расширения, и благодаря строгой типизации мгновенно обнаруживал ошибки при интеграции новых плагинов.
Символы в mapped types и условных типах
Одна из самых мощных особенностей TypeScript — mapped types, позволяющие трансформировать один тип в другой. Когда я впервые обнаружил, что символы отлично работают с этим механизмом, у меня буквально открылись новые горизонты проектирования API.
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const TITLE = Symbol('title');
const CONTENT = Symbol('content');
const AUTHOR = Symbol('author');
type DocumentFields = {
[TITLE]: string;
[CONTENT]: string;
[AUTHOR]: string;
};
// Создаем тип с опциональными полями
type PartialDocument = {
[K in keyof DocumentFields]?: DocumentFields[K];
};
// Создаем тип только для чтения
type ReadonlyDocument = {
readonly [K in keyof DocumentFields]: DocumentFields[K];
}; |
|
В этом примере mapped types позволяют нам создавать различные варианты типов на основе исходного, сохраняя при этом строгую типизацию символьных ключей. Символы особенно хороши для internal API, где мы хотим избежать случайных конфликтов имен. В условных типах символы тоже раскрывают свой потенциал:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const ID = Symbol('id');
const NAME = Symbol('name');
type HasId<T> = T extends { [ID]: unknown } ? true : false;
type HasName<T> = T extends { [NAME]: unknown } ? true : false;
type Entity = {
[ID]: number;
data: string;
};
type WithNameOnly = {
[NAME]: string;
};
// Проверяем типы
type EntityHasId = HasId<Entity>; // true
type EntityHasName = HasName<Entity>; // false
type WithNameOnlyHasId = HasId<WithNameOnly>; // false |
|
Такой подход позволяет создавать по-настоящему типобезопасные API, где компилятор может статически проверить наличие определенных символьных свойств.
Работа с Symbol.iterator и типизация итераторов
Одна из самых практичных областей применения символов — создание кастомных итерируемых объектов. В TypeScript это требует особого внимания к типам:
| 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
| class NumberRange implements Iterable<number> {
constructor(private start: number, private end: number) {}
// Обратите внимание на типизацию метода
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const end = this.end;
return {
next(): IteratorResult<number> {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
// Теперь объект поддерживает итерацию
const range = new NumberRange(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
} |
|
Тут я столкнулся с интересной особенностью. Однажды, пытаясь переписать свою библиотеку для работы с коллекциями на TypeScript, я забыл указать правильные типы для итератора, и компилятор позволил мне создать итератор, который возвращает объекты неправильного типа. Это привело к странным ошибкам во время выполнения. С тех пор я всегда строго типизирую итераторы:
| 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
| interface CustomIterator<T> extends Iterator<T> {
reset(): void; // дополнительный метод
next(): IteratorResult<T>;
}
class EnhancedCollection<T> implements Iterable<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
[Symbol.iterator](): CustomIterator<T> {
let index = 0;
const items = this.items;
return {
reset() {
index = 0;
},
next() {
if (index < items.length) {
return { value: items[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
} |
|
Асинхронные итераторы и Symbol.asyncIterator
С появлением асинхронных генераторов в JavaScript TypeScript добавил поддержку типизации для Symbol.asyncIterator. Это открывает возможности для создания потоков данных, которые асинхронно загружают информацию по мере необходимости:
| 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
31
32
33
34
35
36
37
38
39
40
41
42
43
| class AsyncDataSource implements AsyncIterable<string> {
private data: string[] = [];
private isLoading = false;
async loadMoreData(): Promise<string[]> {
// Имитация загрузки данных
this.isLoading = true;
await new Promise(resolve => setTimeout(resolve, 500));
const newData = [`Item ${this.data.length + 1}`, [INLINE]Item ${this.data.length + 2}[/INLINE]];
this.data.push(...newData);
this.isLoading = false;
return newData;
}
[Symbol.asyncIterator](): AsyncIterator<string> {
let index = 0;
const self = this;
return {
async next(): Promise<IteratorResult<string>> {
if (index >= self.data.length && !self.isLoading) {
await self.loadMoreData();
}
if (index < self.data.length) {
return { value: self.data[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
}
// Использование
async function processData() {
const source = new AsyncDataSource();
for await (const item of source) {
console.log(item);
if (item === 'Item 10') break; // Ограничиваем количество итераций
}
} |
|
Когда я впервые применил этот паттерн в реальном проекте для потоковой обработки больших наборов данных, производительность выросла в разы — мы стали загружать только те данные, которые действительно нужны прямо сейчас.
Символы как альтернатива enum
Одно из моих любимых применений символов — создание enum-подобных структур, но с более строгой типизацией, чем обычные TypeScript enum:
| 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
31
| // Создаем символьные константы
const Red = Symbol('red');
const Green = Symbol('green');
const Blue = Symbol('blue');
// Объединяем их в объект (как enum)
const Color = {
Red,
Green,
Blue
} as const;
// Создаем тип на основе значений объекта
type ColorType = typeof Color[keyof typeof Color];
// Теперь можно использовать в функциях
function paintElement(color: ColorType) {
// Логика окрашивания
if (color === Color.Red) {
console.log('Красим в красный');
} else if (color === Color.Green) {
console.log('Красим в зеленый');
} else if (color === Color.Blue) {
console.log('Красим в синий');
}
}
// Корректное использование
paintElement(Color.Red); // Работает
// Некорректное использование
// paintElement(Symbol('red')); // Ошибка компиляции |
|
Такой подход решает одну из основных проблем стандартных enum в TypeScript — невозможность использовать произвольные строки в качестве значений. Кроме того, при сериализации объекта Color в JSON, мы получим более предсказуемый результат. Вместе с тем, есть и ограничение — символы не сериализуются в JSON. Это может быть как недостатком, так и преимуществом, в зависимости от конкретной задачи.
Проблемы рефлексии и динамический доступ к символьным свойствам
Работая над инфраструктурными библиотеками, я часто сталкиваюсь с необходимостью динамического доступа к свойствам объектов. Здесь символы создают дополнительный уровень сложности из-за особенностей их работы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const SECRET_CONFIG = Symbol('config');
class Service {
[SECRET_CONFIG] = { apiKey: '12345' };
public name = 'Main Service';
}
const service = new Service();
// Обычное перечисление не покажет символьные ключи
Object.keys(service); // ['name']
// Нужен специальный метод для получения символьных ключей
Object.getOwnPropertySymbols(service); // [Symbol(config)]
// Для получения всех ключей (включая символы):
Reflect.ownKeys(service); // ['name', Symbol(config)] |
|
В TypeScript это создает интересный вызов — как правильно типизировать функции, которые должны работать с символьными ключами динамически? Один из подходов, который я использую:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| function getSymbolPropOrDefault<T, K extends symbol, D>(
obj: T,
key: K,
defaultValue: D
): K extends keyof T ? T[K] : D {
return (typeof obj === 'object' && obj !== null && key in obj)
? (obj as any)[key]
: defaultValue as any;
}
// Использование
const value = getSymbolPropOrDefault(service, SECRET_CONFIG, {}); |
|
Хотя приходится использовать any в реализации, интерфейс функции остается типобезопасным для клиентского кода.
Особенности работы с символами в строгом режиме TypeScript
Когда я перешел на строгий режим TypeScript ("strict": true в tsconfig.json), обнаружил несколько интересных нюансов при работе с символами:
1. TypeScript требует явно объявлять символьные индексные сигнатуры.
2. Присваивание между различными типами символов становится более строгим.
3. Работа с well-known символами требует корректной типизации.
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| interface StrictSymbolMap {
// В строгом режиме обязательно указать индексную сигнатуру для символов
[key: symbol]: string;
}
const map: StrictSymbolMap = {};
const key1 = Symbol('one');
const key2 = Symbol('two');
map[key1] = 'Value 1';
map[key2] = 'Value 2';
// В строгом режиме будет ошибка без явного приведения типов:
// map[key1] = 123; // Ошибка: Тип 'number' не может быть присвоен типу 'string' |
|
Самый неожиданный для меня момент связан с доступом к статическим свойствам типа Symbol. В строгом режиме TypeScript требует более точной типизации:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // В строгом режиме нужно правильно типизировать обработчики для well-known символов
class MyClass {
// Тип должен соответствовать спецификации
[Symbol.toStringTag]: string = 'MyClass';
// Для кастомных методов важно указывать правильные сигнатуры
[Symbol.toPrimitive](hint: string): string | number | boolean {
switch (hint) {
case 'number': return 100;
case 'string': return 'Custom string';
default: return true;
}
}
} |
|
TypeScript vs Script# vs У кого какой опыт ? - сравнительные достоинства и недостатки. Перевод C# на TypeScript Доброго времени суток))) (Извините если не в ту тему)
Существует рабочая программы для локального... VS2012 + typescript 9.1.1 При работе с TypeScript VS2012 виснет или закрывается регулярно, никакой конкретной информации об... Создать редактор радиосхем для MVC5, используя TypeScript Ребята!)
Нужна инфа как возможно создать редактор,используя typescript (js нежелательно),на mvc 5...
Практические сценарии использования

Теория это хорошо, но давайте перейдем к реальным применениям символов в рабочих проектах. За свою карьеру я накопил немалый багаж практик, где символы оказывались незаменимым инструментом, и хочу поделиться самыми полезными из них.
Создание приватных свойств объектов
До появления настоящих приватных полей в JavaScript с символом #, символы были (и во многих случаях остаются) лучшим способом эмуляции приватности. Помню, как в одном из проектов нам нужно было добавлять внутренние метаданные к объектам, которые приходили от клиента, не засоряя при этом само API.
| 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
31
32
33
34
| // Символ доступен только внутри модуля
const _metadata = Symbol('metadata');
export class UserProfile {
public name: string;
public email: string;
// Приватное символьное свойство
private [_metadata]: {
lastModified: Date;
accessLevel: number;
internalId: string;
};
constructor(name: string, email: string) {
this.name = name;
this.email = email;
this[_metadata] = {
lastModified: new Date(),
accessLevel: 1,
internalId: crypto.randomUUID()
};
}
// Геттер для части внутренних данных
get modificationDate(): Date {
return this[_metadata].lastModified;
}
// Метод использующий приватные данные
hasAccess(requiredLevel: number): boolean {
return this[_metadata].accessLevel >= requiredLevel;
}
} |
|
Преимущество такого подхода в том, что свойство [_metadata] не будет видно при сериализации объекта в JSON, не появится при перечислении свойств через Object.keys(), и не будет мешать пользователям класса. При этом, в отличие от обычных приватных полей с решеткой (#field), мы можем динамически обращаться к таким свойствам, что критично для создания обобщенных утилит.
Брендирование типов и номинальная типизация
TypeScript использует структурную типизацию, что иногда приводит к неожиданным проблемам. Классический пример — нам нужно различать два типа, которые структурно идентичны, но семантически различны.
Как-то я работал над финансовым приложением, где было критично различать разные типы идентификаторов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // Без брендирования - проблема
type UserId = string;
type AccountId = string;
function processUser(id: UserId) { /* ... */ }
function processAccount(id: AccountId) { /* ... */ }
const userId: UserId = "user-123";
const accountId: AccountId = "acc-456";
// TypeScript не увидит ошибку! Строки структурно одинаковые
processAccount(userId); // Ой! Передали не тот ID |
|
С символами можно создать настоящую номинальную типизацию:
| 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
| // Символы для брендирования
declare const UserIdBrand: unique symbol;
declare const AccountIdBrand: unique symbol;
// Брендированные типы
type UserId = string & { readonly [UserIdBrand]: unique symbol };
type AccountId = string & { readonly [AccountIdBrand]: unique symbol };
// Функции-конструкторы для безопасного создания идентификаторов
function createUserId(id: string): UserId {
return id as UserId;
}
function createAccountId(id: string): AccountId {
return id as AccountId;
}
// Теперь наши функции типобезопасны
function processUser(id: UserId) { /* ... */ }
function processAccount(id: AccountId) { /* ... */ }
const userId = createUserId("user-123");
const accountId = createAccountId("acc-456");
processUser(userId); // Ок
// processAccount(userId); // Ошибка компиляции! |
|
Этот паттерн спас нас от множества потенциальных багов при рефакторинге. Особенно ценно, что во время выполнения тут нет никаких накладных расходов — вся магия происходит на уровне типов.
Создание надежных точек расширения в библиотеках
Когда я разрабатывал библиотеку для обработки данных с поддержкой плагинов, столкнулся с проблемой определения точек расширения. Пользователи библиотеки должны были иметь возможность регистрировать обработчики для разных этапов обработки данных. Традиционно это делается через строковые идентификаторы:
| TypeScript | 1
2
3
| // Проблемный подход со строками
library.registerHandler('beforeProcess', handler);
library.registerHandler('afterValidation', anotherHandler); |
|
Но возникает проблема — что если пользователь сделает опечатку? Или мы переименуем хук в будущих версиях?
Решение с символами:
| 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
31
32
33
34
35
36
37
| // Определяем публичные символы для хуков
export const Hooks = {
BEFORE_PROCESS: Symbol('hooks:beforeProcess'),
AFTER_VALIDATION: Symbol('hooks:afterValidation'),
ON_ERROR: Symbol('hooks:onError'),
COMPLETE: Symbol('hooks:complete')
} as const;
export type HookType = typeof Hooks[keyof typeof Hooks];
// Типы для хендлеров разных хуков
interface BeforeProcessData { source: string; }
interface AfterValidationData { valid: boolean; errors?: string[]; }
interface ErrorData { error: Error; phase: string; }
interface CompleteData { duration: number; recordsProcessed: number; }
// Маппинг типов данных для каждого хука
export interface HookDataMap {
[Hooks.BEFORE_PROCESS]: BeforeProcessData;
[Hooks.AFTER_VALIDATION]: AfterValidationData;
[Hooks.ON_ERROR]: ErrorData;
[Hooks.COMPLETE]: CompleteData;
}
// Теперь наше API типобезопасно
export function registerHandler<T extends HookType>(
hook: T,
handler: (data: HookDataMap[T]) => void
): void {
// Реализация...
}
// Использование
registerHandler(Hooks.BEFORE_PROCESS, (data) => {
// TypeScript знает, что data имеет тип BeforeProcessData
console.log(data.source);
}); |
|
При таком подходе невозможно зарегистрировать обработчик для несуществующего хука, а TypeScript автоматически подсказывает правильные типы данных для каждого конкретного обработчика. Я использовал этот паттерн во множестве проектов и могу сказать, что он невероятно надежен при рефакторинге.
Метапрограммирование с well-known символами
Well-known символы — особый тип символов, предопределенный в JavaScript. Они позволяют переопределять поведение различных языковых конструкций. В TypeScript мы получаем типобезопасное API для работы с ними.
Наибольший успех у меня был с переопределением Symbol.toPrimitive, что позволило создать умные объекты, которые ведут себя по-разному в различных контекстах:
| 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
31
32
33
34
35
36
37
38
39
40
41
| class SmartNumber {
constructor(private value: number) {}
// Определяем поведение при приведении к примитиву
[Symbol.toPrimitive](hint: string): any {
switch (hint) {
case 'number':
return this.value;
case 'string':
return `SmartNumber(${this.value})`;
case 'default':
return this.value.toString();
}
}
// Реализуем свой итератор для перебора цифр числа
[Symbol.iterator](): Iterator<number> {
const digits = this.value.toString().split('').map(Number);
let index = 0;
return {
next() {
if (index < digits.length) {
return { value: digits[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
}
const smartNum = new SmartNumber(12345);
// Используем в разных контекстах
const doubled = smartNum * 2; // 24690
const greeting = `Value: ${smartNum}`; // "Value: SmartNumber(12345)"
// Итерация по цифрам
for (const digit of smartNum) {
console.log(digit); // Выведет 1, 2, 3, 4, 5
} |
|
Другой полезный случай — `Symbol.hasInstance`, который позволяет настроить поведение оператора instanceof:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class VersionRange {
constructor(private min: string, private max: string) {}
// Кастомное поведение instanceof
static [Symbol.hasInstance](instance: any): boolean {
if (typeof instance !== 'string') return false;
// Проверяем, соответствует ли строка формату версии
return /^\d+\.\d+\.\d+$/.test(instance);
}
}
// Теперь можем делать такие проверки
console.log('1.2.3' instanceof VersionRange); // true
console.log('1.2' instanceof VersionRange); // false
console.log(123 instanceof VersionRange); // false |
|
Безопасное хранение метаданных для декораторов
В одном из проектов мне потребовалось реализовать систему декораторов для валидации входных данных REST API. Проблема была в том, чтобы где-то хранить метаданные о валидации, не засоряя классы дополнительными свойствами. Символы идеально подошли для этой задачи:
| 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| const validationMetadataKey = Symbol('validation:metadata');
interface ValidationRule {
property: string;
type: 'required' | 'minLength' | 'maxLength' | 'pattern' | 'custom';
value?: any;
message?: string;
validator?: (value: any) => boolean;
}
// Декоратор для добавления правил валидации
function Validate(rule: Omit<ValidationRule, 'property'>) {
return function(target: any, propertyKey: string) {
const rules: ValidationRule[] = Reflect.getMetadata(validationMetadataKey, target.constructor) || [];
rules.push({
...rule,
property: propertyKey
});
Reflect.defineMetadata(validationMetadataKey, rules, target.constructor);
};
}
// Пример использования
class UserDTO {
@Validate({ type: 'required', message: 'Name is required' })
@Validate({ type: 'minLength', value: 3, message: 'Name must be at least 3 characters' })
name!: string;
@Validate({ type: 'pattern', value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email format' })
email!: string;
}
// Функция валидации
function validate<T>(instance: T): { valid: boolean, errors: string[] } {
const errors: string[] = [];
const constructor = instance.constructor;
const rules: ValidationRule[] = Reflect.getMetadata(validationMetadataKey, constructor) || [];
for (const rule of rules) {
const value = (instance as any)[rule.property];
let valid = true;
switch (rule.type) {
case 'required':
valid = value !== undefined && value !== null && value !== '';
break;
case 'minLength':
valid = value?.length >= rule.value;
break;
case 'pattern':
valid = rule.value.test(value);
break;
case 'custom':
valid = rule.validator!(value);
break;
}
if (!valid && rule.message) {
errors.push(rule.message);
}
}
return { valid: errors.length === 0, errors };
} |
|
Символы обеспечивают надежную изоляцию метаданных, предотвращая конфликты имен и случайный доступ. В больших проектах это критически важно, особено когда несколько библиотек используют одну и ту же технику декораторов.
Внутренние API библиотек
Разрабатывая сложные библиотеки, постоянно сталкиваюсь с дилеммой: как предоставить продвинутым пользователям доступ к внутренним функциям, не загромождая основное API? Символы предлагают элегантное решение:
| 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
| // internal.ts - не экспортируется напрямую
export const InternalAPI = {
CACHE_ACCESS: Symbol('internal:cache'),
BYPASS_HOOKS: Symbol('internal:bypass-hooks'),
RAW_CONNECTION: Symbol('internal:connection')
};
// library.ts - основной публичный API
import { InternalAPI } from './internal';
export class DatabaseClient {
// Публичное API
connect() { /* ... */ }
query() { /* ... */ }
// Внутреннее API, доступное только продвинутым пользователям,
// которые явно импортировали InternalAPI
[InternalAPI.CACHE_ACCESS]() {
// Прямой доступ к кэшу запросов
}
[InternalAPI.RAW_CONNECTION]() {
// Доступ к низкоуровневому соединению
}
}
// Опционально можем экспортировать internal API отдельно
export { InternalAPI as Internal }; |
|
Такой подход позволяет создавать многоуровневые API: простое для обычных пользователей и расширенное для тех, кто готов глубже погрузиться в работу библиотеки. При этом случайное использование внутреннего API практически исключено.
Подводные камни и ограничения
Как и любая мощная функция языка, символы в TypeScript имеют свои недостатки и ограничения, которые могут превратить ваш код в минное поле, если вы о них не знаете. Я столкнулся с большинством из них на собственном опыте — и некоторые стоили мне целых дней отладки. Давайте рассмотрим основные проблемы и способы их обхода.
Проблемы сериализации JSON
Самый очевидный и болезненный недостаток символов — полная невозможность их сериализации. Пару лет назад я попал в неприятную ситуацию, когда проектировал систему кэширования состояния приложения с использованием символов как уникальных ключей. Всё работало прекрасно, пока не потребовалось сохранять состояние в localStorage:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const USER_STATE = Symbol('userState');
const APP_SETTINGS = Symbol('appSettings');
const state = {
[USER_STATE]: { name: "John", permissions: ["admin"] },
[APP_SETTINGS]: { theme: "dark", notifications: true }
};
// При попытке сохранить...
localStorage.setItem('appState', JSON.stringify(state));
// ...и восстановить
const restoredState = JSON.parse(localStorage.getItem('appState') || '{}');
console.log(restoredState); // {} - пустой объект! |
|
Проблема в том, что JSON.stringify() просто игнорирует символьные ключи. Полностью. И, хотя в некоторых случаях это может быть преимуществом (например, когда мы не хотим сериализовать "приватные" данные), чаще всего это вызывает серьёзные проблемы.
После многих экспериментов я пришел к двум основным решениям:
1. Механизм маппинга: Хранить соответствие между символами и строками
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const symbolMap = {
[Symbol('userState').toString()]: 'USER_STATE',
[Symbol('appSettings').toString()]: 'APP_SETTINGS'
};
function serializeWithSymbols(obj: any): string {
const replacer = (key: string, value: any) => {
// Преобразуем символьные ключи в строки при сериализации
if (typeof key === 'symbol') {
return { __isSymbol: true, key: symbolMap[key.toString()] };
}
return value;
};
return JSON.stringify(obj, replacer);
} |
|
2. Использовать символы только для прототипных методов, а для данных использовать строки:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Безопасно для сериализации
const userState = {
id: 'user-123',
name: 'John',
settings: {
theme: 'dark'
}
};
// Используем символы только для методов, не для данных
class StateManager {
[Symbol.for('internal:save')](state: any) {
localStorage.setItem('state', JSON.stringify(state));
}
} |
|
Оба решения несовершенны, и я всегда рекомендую хорошенько подумать, прежде чем использовать символы для данных, которые в будущем могут потребовать сериализации.
Отладка кода с символьными ключами
Ещё один "сюрприз" ждёт нас при отладке. Я как-то провел целый день, пытаясь понять, почему мой объект не имеет ожидаемых свойств. Дело было в том, что свойства с символьными ключами по умолчанию скрыты в большинстве инструментов отладки.
| TypeScript | 1
2
3
4
| const KEY = Symbol('someKey');
const obj = { [KEY]: 'hidden value' };
console.log(obj); // В консоли браузера: {} |
|
Чтобы увидеть символьные свойства в Chrome DevTools, нужно развернуть объект и искать специальную секцию "Symbol properties". В Node.js консоли символьные свойства отображаются несколько иначе, но всё равно не так очевидно, как обычные.
Для упрощения отладки я рекомендую:
1. Всегда давать символам содержательные описания.
2. Создавать вспомогательные функции для работы с символьными свойствами:
| TypeScript | 1
2
3
4
5
6
7
| function debugObject(obj: any): void {
console.log('Regular properties:', Object.getOwnPropertyNames(obj));
console.log('Symbol properties:', Object.getOwnPropertySymbols(obj).map(sym => ({
symbol: sym.toString(),
value: obj[sym]
})));
} |
|
Потеря типа при объединении объектов
Об этой проблеме я узнал на собственном опыте, когда работал над библиотекой управления состоянием. Оказывается, при объединении объектов (через spread-оператор или Object.assign()) TypeScript может "терять" типы символьных ключей:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const ID = Symbol('id');
interface Entity {
name: string;
[ID]: string;
}
const original: Entity = {
name: "User",
[ID]: "12345"
};
// Создаём копию с новым именем
const copy = { ...original, name: "Admin" };
// Ошибка! Property 'ID' does not exist on type...
console.log(copy[ID]); |
|
Проблема в том, что типы символьных свойств не всегда корректно переносятся при операциях spread. Решение - явное указание типа:
| TypeScript | 1
2
| const copy: Entity = { ...original, name: "Admin" };
// Теперь TypeScript знает, что copy содержит свойство [ID] |
|
Совместимость с различными окружениями
Символы появились в ES2015, и хотя большинство современных браузеров их поддерживают, старые версии IE и некоторые мобильные браузеры могут вызвать проблемы. Однажды я потратил полдня на расследование странных ошибок в приложении, пока не выяснил, что клиент тестировал его в IE11 без полифиллов.
Если вам нужно поддерживать старые браузеры, вам понадобится полифилл для Symbol, но есть нюанс — большинство полифиллов не обеспечивают истинную уникальность символов, а используют генерацию случайных строк. Это работает для базовых случаев, но может привести к непредсказуемым результатам в сложных сценариях, особенно с well-known символами.
Проблемы с регистрами символов и garbage collection
Symbol.for() создаёт символы в глобальном регистре, и они не могут быть собраны сборщиком мусора, пока существует ссылка на регистр. Это может привести к утечкам памяти, если вы динамически создаете много глобальных символов.
Пример потенциальной проблемы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| function createDynamicComponent(id: string) {
// Создаём новый символ для каждого компонента
const componentSymbol = Symbol.for(`component:${id}`);
// ...
}
// Если вызывать эту функцию с разными id тысячи раз,
// все созданные символы останутся в памяти навсегда!
for (let i = 0; i < 10000; i++) {
createDynamicComponent(`dynamic-${i}`);
} |
|
Решение — использовать локальные символы (Symbol() без .for()) для динамически создаваемых идентификаторов, если вам не требуется доступ к ним из разных частей кода.
Отсутствие рефлексии для типов unique symbol
В TypeScript тип unique symbol существует только на этапе компиляции. Во время выполнения нет способа определить, был ли конкретный символ объявлен с типом unique symbol. Это может создать проблемы при написании обобщенного кода для работы с символами разных типов. Например, вы не можете написать функцию, которая по-разному обрабатывает обычные и уникальные символы — для среды выполнения они неразличимы.
Проблемы с тестированием
При написании модульных тестов для кода, использующего символы, возникают специфические проблемы. В частности, затруднительно проверять наличие и значения свойств с символьными ключами:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Функция, которую мы тестируем
function enhanceUser(user: any) {
const META = Symbol('meta');
return {
...user,
[META]: { lastModified: new Date() }
};
}
// В тесте нам нужно как-то получить доступ к META
test('enhanceUser adds metadata', () => {
const enhanced = enhanceUser({ name: 'Test' });
// Как проверить символьное свойство? У нас нет доступа к META!
}) |
|
Решение обычно включает экспорт символов из модуля или использование Object.getOwnPropertySymbols() для получения всех символьных ключей объекта.
Пример приложения
Давайте соберем всё вместе и создадим небольшое, но законченное приложение, которое демонстрирует силу символов в реальном мире. Я разработаю простую систему плагинов для редактора документов — задачу, с которой столкнулся в одном из своих проектов, когда нужно было обеспечить безопасное расширение функциональности без конфликтов между компонентами. Наша система будет построена на символах для определения точек расширения и обеспечения типобезопасности при регистрации плагинов:
| 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // core.ts - Ядро нашей системы плагинов
// Определяем точки расширения через символы
export const PluginHooks = {
DOCUMENT_LOAD: Symbol('plugin:document:load'),
DOCUMENT_SAVE: Symbol('plugin:document:save'),
TEXT_TRANSFORM: Symbol('plugin:text:transform'),
RENDER_AUGMENTATION: Symbol('plugin:render:augment')
} as const;
// Типы данных для каждого хука
export interface DocumentData {
content: string;
metadata: Record<string, unknown>;
}
// Типизированные интерфейсы для каждого типа плагина
export interface PluginHandlerMap {
[PluginHooks.DOCUMENT_LOAD]: (data: DocumentData) => DocumentData;
[PluginHooks.DOCUMENT_SAVE]: (data: DocumentData) => DocumentData;
[PluginHooks.TEXT_TRANSFORM]: (text: string) => string;
[PluginHooks.RENDER_AUGMENTATION]: (element: HTMLElement, content: string) => void;
}
// Менеджер плагинов с типобезопасной регистрацией
export class PluginManager {
private handlers: Map<symbol, Array<any>> = new Map();
// Типобезопасная регистрация обработчиков
registerHandler<T extends keyof PluginHandlerMap>(
hook: T,
handler: PluginHandlerMap[T]
): void {
if (!this.handlers.has(hook)) {
this.handlers.set(hook, []);
}
this.handlers.get(hook)!.push(handler);
}
// Типобезопасное выполнение хуков
executeHook<T extends keyof PluginHandlerMap>(
hook: T,
...args: Parameters<PluginHandlerMap[T]>
): ReturnType<PluginHandlerMap[T]> | undefined {
const handlers = this.handlers.get(hook) || [];
let result: any = args[0];
for (const handler of handlers) {
result = handler(result);
}
return result;
}
} |
|
Далее создадим редактор документов, который интегрируется с этой системой плагинов:
| 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| // editor.ts - Редактор с поддержкой плагинов
import { PluginManager, PluginHooks, DocumentData } from './core';
export class DocumentEditor {
private plugins: PluginManager;
private currentDocument: DocumentData = {
content: '',
metadata: {}
};
constructor() {
this.plugins = new PluginManager();
}
// Публичный API для регистрации плагинов
use<T extends keyof PluginHandlerMap>(
hook: T,
handler: PluginHandlerMap[T]
): this {
this.plugins.registerHandler(hook, handler);
return this;
}
// Загрузка документа с обработкой плагинами
loadDocument(rawData: DocumentData): void {
// Применяем плагины обработки загрузки
this.currentDocument = this.plugins.executeHook(
PluginHooks.DOCUMENT_LOAD,
rawData
) || rawData;
this.render();
}
// Сохранение документа с обработкой плагинами
saveDocument(): DocumentData {
return this.plugins.executeHook(
PluginHooks.DOCUMENT_SAVE,
this.currentDocument
) || this.currentDocument;
}
// Рендеринг с учетом плагинов
private render(): void {
const container = document.getElementById('editor') as HTMLElement;
if (!container) return;
// Применяем текстовые трансформации
let content = this.plugins.executeHook(
PluginHooks.TEXT_TRANSFORM,
this.currentDocument.content
) || this.currentDocument.content;
container.innerHTML = content;
// Применяем расширения рендеринга
this.plugins.executeHook(
PluginHooks.RENDER_AUGMENTATION,
container,
content
);
}
} |
|
Наконец, посмотрим как будут выглядеть плагины для этого редактора:
| 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // plugins.ts - Примеры плагинов
import { PluginHooks } from './core';
import { DocumentEditor } from './editor';
// Плагин автоматического сохранения версий
const versioningPlugin = (editor: DocumentEditor) => {
editor.use(PluginHooks.DOCUMENT_SAVE, (data) => {
return {
...data,
metadata: {
...data.metadata,
version: ((data.metadata.version as number) || 0) + 1,
lastSaved: new Date().toISOString()
}
};
});
};
// Плагин для синтаксического выделения кода
const syntaxHighlightPlugin = (editor: DocumentEditor) => {
// Трансформация текста для подсветки кода
editor.use(PluginHooks.TEXT_TRANSFORM, (text) => {
return text.replace(
/`{3}(typescript|js)([\s\S]*?)`{3}/g,
'<pre class="code-block">$2</pre>'
);
});
// Добавление стилей после рендеринга
editor.use(PluginHooks.RENDER_AUGMENTATION, (element) => {
const codeBlocks = element.querySelectorAll('.code-block');
codeBlocks.forEach(block => {
// Применяем подсветку синтаксиса
highlightCode(block as HTMLElement);
});
});
};
// Вспомогательная функция подсветки кода
function highlightCode(element: HTMLElement): void {
// Реализация подсветки синтаксиса
}
// Использование
const editor = new DocumentEditor();
versioningPlugin(editor);
syntaxHighlightPlugin(editor); |
|
Что я получил в результате? Полностью типобезопасную систему плагинов, где:
1. Невозможно зарегистрировать обработчик для несуществующего хука,
2. TypeScript автоматически определяет правильные типы параметров и возвращаемых значений,
3. Символы обеспечивают уникальность точек расширения, предотвращая конфликты,
4. Система легко расширяема — добавление новых точек расширения не ломает существующий код.
В реальном приложении я бы добавил еще много фичей: приоритеты плагинов, возможность отключать плагины, обработку ошибок. Но даже в таком упрощенном виде преимущества символов очевидны — они обеспечивают надежную и типобезопасную систему точек расширения без хрупких строковых идентификаторов.
Разбиение скомилированного Typescript на файлы В проекте имеется множество typescript файлов, которые компилируются в один js. Но часть скриптов... Изучение TypeScript - советы Нуждаюсь в срочном освоении TypeScript. Поделитесь ресурсами, пожалуйста. Можно на русском и... Не понятен пример кода из спецификации TypeScript Читаю про объектные типы в спецификации на странцие 13, но не понятно из описания как устроен и... Передать свойство класса в анонимную функцию TypeScript как передать значение свойства класса в анонимную функцию.
например следующий код работает... TypeScript "PreComputeCompileTypeScript" how to fix in project Выскакивает ошибка
Везде исправление данной ошибки идёт путём редактирования файла ... Решение кольцевых зависимостей TypeScript + RequreJS Всем привет, возникла проблема с кольцевыми зависимостями при использовании наследования в... TypeScript | extends error Собственно, сделал такой класс:
class trueDate extends Date {
constructor(date: string)... Сохранение this в TypeScript Доброго дня. Подскажите пожалуйста, как можно сохранить this класса так, чтобы можно было... C# класс в TypeScript класс (перенос сущностей) делается бекенд на C#, фронтенд на ts, общаются через REST API (http)
сериализация обьектов... Лучшие видео ресурсы о программировании, angualr 2, typeScript, react и все сомое вкусное Всем доброй поры времени.
Я нашел новый для себя, и как не странно, вообще новый ресур, с видео... Как проводится отладка при использовании TypeScript Присматриваюсь к Angular 2. Из прочитанных статей сложилось впечатление, что в мире Angular 2 есть... Создать строку типа массив. Typescript Добрый день ув. пользователи! Подскажите пожалуйста, как в цикле заполнить данными items типа...
|