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

Комментарии 6

Привет, есть вопрос по типизации, не очень понятно зачем расширять ручками интерфейсы, если тип к emitEvent можно высчитать автоматически

что я имею ввиду

const OUR_FIRST_ONBOARDING = {
    id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
    steps: [
        {
            id: "ID_ПЕРВОГО_ШАГА",
            type: StepTypes.CLICK,
            views: {
                pointer: {
                    type: PointerType.CANVAS,
                    elementId: 'ЭЛЕМЕНТ_НА_КОТОРЫЙ_ХОТИМ_ПОЛУЧИТЬ_КЛИК',
                },
                progress: {
                    current: 1,
                    total: 12,
                },
            },
        },
        {
            id: "ID_ВТОРОГО_ШАГА",
            type: StepTypes.INPUT,
            text: "текст, который нужно ввести",
            views: {
                pointer: {
                    type: PointerType.DOM,
                    elementId: 'ЭЛЕМЕНТ_НА_КОТОРЫЙ_ХОТИМ_ПОЛУЧИТЬ_ВВОД',
                },
                progress: {
                    current: 2,
                    total: 12,
                },
            },
        }
    // Еще 10 шагов
    ]
} as const;


type StepIDS = typeof OUR_FIRST_ONBOARDING.steps[number]['id'];

interface Event { 
    type: string,
    eventId: StepIDS
}

function emitEvent(id: Event) {

}

ну или вариант с несколькими конфигами

interface Event<T extends string> { 
    type: string,
    eventId: T
}


function emitEvent<CompanyStepIds extends string>(id: Event<CompanyStepIds>) {}
emitEvent<StepIDS>({
    type: '1',
    eventId: 'ID_ПЕРВОГО_ШАГА'
})

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

А дальше используешь его так:

// Конфиг
const OUR_FIRST_ONBOARDING = {
	id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
	steps: [
		new StepClick{
			id: "ID_ПЕРВОГО_ШАГА",
		}
	]
}


// Вызов события в основном приложении
OUR_FIRST_ONBOARDING.steps[0].emitEvent()

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

я бы сделал как-то так, но возможно вы так пробовали и вам не зашло

class StepEvent {
	constructor(public id: string) {}

	emit() {}
}

class StepClick extends StepEvent {}

///

interface ConfigEvent {
	type: 'click',
	id: string,
}

interface Config {
	id: string,
	steps: ConfigEvent[]
}

/// 

const OUR_FIRST_ONBOARDING: Config = {
	id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
	steps: [
		{
			type: 'click',
			id: 'ID_ПЕРВОГО_ШАГА',
		}
	]
}

class Service {
	private events: StepEvent[]
	
	constructor (private config: Config) {
		this.events = config.steps.map(this.mapConfigEventToStepEvent)
	}

	private mapConfigEventToStepEvent(configEvent: ConfigEvent) {
		return new StepClick(configEvent.id);
	}

	

	emitEvent(stepNum: number) {
		const event = this.events[stepNum];
		
		if (!event) {
			return;
		}
		
		event.emit();
	}
}




const service = new Service(OUR_FIRST_ONBOARDING);
service.emitEvent(0);

В первой версии у нас так и было, но здесь будут проблемы:

  1. mapConfigEventToStepEvent будет бесконечно расширяемой мапой из-за того что у нас будут появляться новый шаги. Метод приватный и нужно будет писать какие-то интеграционные тесты, чтобы проверить правильность работы класса. Из-за раздуваемости этой мапы, тесты будут очень хрупкие и и них будет увесистая фаза Arrange

  2. Создавая сразу инстанс класса для шага в конфиге мы можем спрятать свойства необходимые для этого класса внутрь


const OUR_FIRST_ONBOARDING = {
	id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
	steps: [
		{
			id: "ID_ПЕРВОГО_ШАГА",
      // Этот параметр спрячется в классе
			type: StepTypes.CLICK,
		}
	]
}


const OUR_FIRST_ONBOARDING = {
	id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
	steps: [
		new StepClick{
			id: "ID_ПЕРВОГО_ШАГА",
		}
	]
}
const EventTypes = {
	click: 'click',
	dbClick: 'dbClick',
} as const;

type EventType = keyof typeof EventTypes;

interface ConfigEvent {
	type: EventType,
	id: string,
}

interface Config {
	id: string,
	steps: ConfigEvent[]
}

/// 

const OUR_FIRST_ONBOARDING: Config = {
	id: "НАШ_ПЕРВЫЙ_ОНБОРДИНГ",
	steps: [
		{
			type: EventTypes.click,
			id: 'ID_ПЕРВОГО_ШАГА',
		}
	]
}

class Service {
	private events: StepEvent[]

	/*
   * можно вынести сюда Классы по типу
   * и тут будет проверка на этапе компиляции
   * а если бутет новый эвент, то эта строка стразу подсветится
   */
	private eventClasses: Record<EventType, typeof StepEvent> = {
		click: StepClick,
		dbClick: StepClick,
	}
	
	constructor (private config: Config) {
		this.events = config.steps.map(this.mapConfigEventToStepEvent)
	}

	// если будет eventClasses, то этот метод вообще не придется трогать
	private mapConfigEventToStepEvent(configEvent: ConfigEvent) {
		const EventClass = this.eventClasses[configEvent.type];
		return new EventClass(configEvent.id);
	}

	

	emitEvent(stepNum: number) {
		const event = this.events[stepNum];
		
		if (!event) {
			return;
		}
		
		event.emit();
	}
}

В любом случае, я понимаю почему вы так сделали и спасибо за дискусию, просто не удержался вставить свои 5 копеек =(

хаха, лайк

Зарегистрируйтесь на Хабре, чтобы оставить комментарий