Разбор сложного сопоставленного типа

В TypeScript есть полезный шаблон порожденных типов, называемый сопоставленным типом. Давайте разберемся в нем, начав с первых принципов и дойдя до полного примера.


Во время просмотра видеоролика Мэтта Покока об использовании DECLARE GLOBAL для удивительного вывода типов я застрял, пытаясь понять тип event в GlobalReducer.

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

Ниже приводится описание этого процесса.

Для начала, вот GlobalReducerEvent, из которого мы можем добавлять и удалять типы reducer.

interface GlobalReducerEvent {
    ADD_TODO: {
        text: string
    }
    LOG_IN: {
        email: string
    }
    DELETE_TODO: {
        todo_id: number
    }
}

type ReducerEvent = ... /* what we're about to build */

type GlobalReducer<TState> = (
    state: TState,
    event: ReducerEvent
) => TState
Вход в полноэкранный режим Выход из полноэкранного режима

Мы хотим, чтобы наш GlobalReducer выполнял проверку типов на основе того, какие события включены. Для этого нам понадобится использовать некоторые интересные возможности TypeScript, которые позволят нам отобразить GlobalReducerEvent в тип, который будет полезен в нашем пользовательском коде.

// this is our target type
type ReducerEvent = ({
    type: "ADD_TODO";
    text: string;
}) | ({
    type: "LOG_IN";
    email: string;
}) | ({
    type: "DELETE_TODO";
    todo_id: number;
})
Вход в полноэкранный режим Выход из полноэкранного режима

Построение до ReducerEvent (тип event) является нашей конечной целью.

Начиная с keyof, мы можем получить тип объединения всех событий.

// What are our different types of events?
type EventUnion = keyof GlobalReducerEvent;
/*
type EventUnion = 'ADD_TODO' | 'LOG_IN' | 'DELETE_TODO'
*/
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

С помощью in мы можем создать индексную сигнатуру над значениями из EventUnion (который, напомню, является keyof GlobalReducerEvent). На данный момент мы будем вводить каждый из этих ключей как any.

// Gather up the types of events
// (we'll replace the `any` in a moment)
type EventTypes = {
    [EventType in EventUnion]: any
};
/*
type EventTypes = {
    ADD_TODO: any;
    LOG_IN: any;
    DELETE_TODO: any;
}

same as doing:

type EventTypes = {
    [EventType in keyof GlobalReducerEvent]: any
}
*/
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь вместо того, чтобы вводить их как any, давайте привяжем их к объекту. Этот объект начнет создавать значения, которые мы пытаемся отобразить.

Каждое событие должно иметь type, значение которого соответствует его имени. EventType является ссылкой на имя события, поэтому мы можем использовать его в типе объекта ({ type: EventType }).

// Event type keyed to itself as an object
// (weird intermediate type, stick with me here)
type EventTypesWithSelf = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    }
};
/*
type EventTypesWithSelf = {
    ADD_TODO: {
        type: "ADD_TODO";
    };
    LOG_IN: {
        type: "LOG_IN";
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
    };
}
*/
Вход в полноэкранный режим Выход из полноэкранного режима

Каждое событие имеет тип, определяющий данные, которые должны сопровождаться этим событием. Например, ADD_TODO требует немного текста, который представляет собой то, что должно быть сделано. Следовательно, ADD_TODO: { text: string }. В этой следующей части мы будем вводить типы для данных каждого события.

Мы можем использовать литерал типа пересечения (&) для объединения каждого типа события ({type: 'ADD_TODO'}) с его типом данных ({text: string}).

// Event type keyed to itself and its data
// (still looks weird, but starting to take shape)
type EventTypesWithSelfAndData = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    } & GlobalReducerEvent[EventType]
};
/*
type EventTypesWithSelfAndData = {
    ADD_TODO: {
        type: "ADD_TODO";
    } & {
        text: string;
    };
    LOG_IN: {
        type: "LOG_IN";
    } & {
        email: string;
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
    } & {
        todo_id: number;
    };
}

which you can think of as:

type EventTypesWithSelfAndData = {
    ADD_TODO: {
        type: "ADD_TODO";
        text: string;
    };
    LOG_IN: {
        type: "LOG_IN";
        email: string;
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
        todo_id: number;
    };
}
*/
Вход в полноэкранный режим Выход из полноэкранного режима

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

Это работает примерно так же, как доступ к значению ключа из объекта JavaScript. Используя тип из предыдущего шага, мы можем получить только тип 'ADD_TODO'.

// Let's try an indexed access of EventTypesWithSelfAndData
type AddTodoType = EventTypesWithSelfAndData['ADD_TODO']
/*
type AddTodoType = {
    type: "ADD_TODO";
} & {
    text: string;
}
*/
Вход в полноэкранный режим Выход из полноэкранного режима

Отличием от того, как работает доступ к объектам JavaScript, является передача типа объединения вместо отдельного значения.

// What if we try to access multiple types at once with a union
type SomeEventTypes = EventTypesWithSelfAndData['ADD_TODO' | 'DELETE_TODO']
/*
type SomeEventTypes = ({
    type: "ADD_TODO";
} & {
    text: string;
}) | ({
    type: "DELETE_TODO";
} & {
    todo_id: number;
})
*/
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что результирующий тип — это тип объединения, состоящий только из типов, индексированных по значениям объединения.

Мы можем пойти еще дальше, сделав индексированный доступ с помощью keyof GlobalReducerEvent, который является объединением всех наших типов событий.

// That means we can pass in a union of all our event type names
// to get a union of all the type signatures.
type AllEventTypes = EventTypesWithSelfAndData[keyof GlobalReducerEvent]
/*
type AllEventTypes = ({
    type: "ADD_TODO";
} & {
    text: string;
}) | ({
    type: "LOG_IN";
} & {
    email: string;
}) | ({
    type: "DELETE_TODO";
} & {
    todo_id: number;
})

which, you might remember, is equivalent to this:

type AllEventTypes = ({
    type: "ADD_TODO";
    text: string;
}) | ({
    type: "LOG_IN";
    email: string;
}) | ({
    type: "DELETE_TODO";
    todo_id: number;
})
*/
Вход в полноэкранный режим Выход из полноэкранного режима

Последний пример эквивалентен этому полному ReducerEvent. Мы собрали его из всех составных частей. Весело!

Давайте посмотрим на все это еще раз.

type ReducerEvent = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    } & GlobalReducerEvent[EventType]
}[keyof GlobalReducerEvent]
Вход в полноэкранный режим Выход из полноэкранного режима

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

Если вам понравилась эта статья, присоединяйтесь к моей рассылке или следите за мной в twitter. Если это помогло вам или у вас есть вопрос, не стесняйтесь написать мне куда угодно. Я с удовольствием послушаю вас!

Хотите поиграть с этим более подробно? Проверьте это на TypeScript Playground.


Обложка фото GeoJango Maps на Unsplash

Оцените статью
Procodings.ru
Добавить комментарий