Typescript: универсальная функция, обеспечивающая безопасный тип доступа к переменной, не экспортируемой из модуля - PullRequest
2 голосов
/ 14 июня 2019

У меня есть модуль с объектом, содержащим кеш элементов разных типов, каждый из которых хранится в свойстве объекта кеша:

type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}

У меня есть функция для извлечения данных из кеша.Он принимает ключ, представляющий свойство кэша (т. Е. pets или humans), и универсальный тип, указывающий тип данных, которые, как я ожидаю, должно возвращать это свойство (т. Е. Pet или Human).Я могу использовать условные типы для обеспечения безопасности типов здесь:

// Alias for all properties of TObj which return a TResult
export type PropertiesOfType<TObj, TResult> = {
    [K in keyof TObj]: TObj[K] extends TResult ? K : never
}[keyof TObj]

function readCache<T, K extends PropertiesOfType<Cache, T[]>, C extends { [key in K]: T[] }>(
    key: K, cache: C
): T[] {
    return cache[key]
}

const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Human[] = readCache("pets", theCache) // Doesn't compile - this is correct

Внутри модуля все работает нормально, так как я могу передать theCache в функцию readCache.Тем не менее, theCache не экспортируется, так как я хочу сохранить его в секрете для модуля.Вместо этого я хочу экспортировать функцию, которую другие модули могут вызывать, чтобы получить доступ только для чтения к данным в кэше, например, что-то вроде этого:

export function getCachedItems<T, K extends PropertiesOfType<Cache, T[]>>(key: K): T[] {
    return readCache(key, theCache) // Doesn't compile
}

Проблема в том, что приведенный выше код не компилируется:

TS2345: Argument of type 'Cache' is not assignable to
parameter of type '{ [key in K]: T[]; }'

Есть ли способ получить код для компиляции (без необходимости небезопасного приведения)?

ОБНОВЛЕНИЕ : Тициан Черникова-Драгомир сделалдва отличных предложения:

  1. Возврат Cache[K].Это хорошо работает для мест, где я хочу вызвать метод и узнать тип, с которым я работаю.Но в случаях, когда у меня есть другая универсальная функция, которая хочет вызвать универсальный getCachedItems, и знаю, что он возвращает массив T, это не сработает.
  2. Return Cache[K][number][],Это решает вышеуказанную проблему (и отвечает на оригинальный вопрос).Но это работает только тогда, когда кэш содержит массивы.Что делать, если я хочу, чтобы объект кеша немного отличался (больше похож на словарь или карту в других языках): каждая запись в кеше должна сама быть другим объектом, со свойством для каждого элемента, который она кеширует, где имя свойстваэто некоторый идентификатор этого объекта, , как предложено при проектировании магазина Redux .

Другими словами, у меня будет это:

export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}

function readCache<T, K extends PropertiesOfType<Cache, Map<T>>, C extends { [key in K]: Map<T> }>(
    key: K, cache: C
): Map<T> {
    return cache[key]
}

const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Map<Human> = readCache("pets", theCache) // Doesn't compile - this is correct

export function getCachedItems<T, K extends PropertiesOfType<Cache, Map<T>>>(key: K): Map<T> {
    return readCache(key, theCache) // Doesn't compile
}

Так что явсе еще получаю оригинальную проблему таким образом.Если я попытаюсь предложить возвращение C[K], это снова сработает, если я знаю тип при вызове, но не в универсальной функции:

function readCache<T, K extends keyof C, C extends { [key in K]: Map<T> }>(
    key: K, cache: C
): C[K] {
    return cache[key]
}

const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Map<Human> = readCache("pets", theCache) // Doesn't compile - this is correct

export function getCachedItems<T, K extends keyof Cache>(key: K): Cache[K] {
    return readCache(key, theCache)
}

const pets2: Map<Pet> = getCachedItems("pets") // Compiles - this is correct
const wrongType2: Map<Human> = getCachedItems("pets") // Doesn't compile - this is correct

function callerInAnotherModule<T, K extends keyof Cache>(key: K) {
    const expected : Map<T> = getCachedItems(key) // Doesn't compile
}

1 Ответ

1 голос
/ 14 июня 2019

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

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

type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}

function readCache<K extends keyof C, C extends { [key in K]: any[] }>(
    key: K, cache: C
): C[K] {
    return cache[key]
}

const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct
const wrongType: Human[] = readCache("pets", theCache)

export function getCachedItems<K extends keyof Cache>(key: K): Cache[K]  {
    return readCache(key, theCache) // ok
}

Хотя приведенный выше ответ работает, запрос в комментариях должен обрабатывать результат readCache как массив. Хотя Cache[K] является объединением массивов, его методы искажены и сложны в использовании. Мы можем развернуть еще один уровень и получить тип элемента из Cache[K], используя Cache[K][number], и использовать его в качестве элемента массива в результате. Это сделает массивы хорошо работающими в таких методах, как getCachedItems:

type Pet = { id: string; name: string, petFood: string }
type Human = { id: string; firstName: string, lastName: string }
type Cache = { pets: Pet[], humans: Human[] }
const theCache: Cache = {pets: [], humans: []}

function readCache<K extends keyof C, C extends { [key in K]: any[] }>(
    key: K, cache: C
): C[K][number][] {
    return cache[key]
}

const pets: Pet[] = readCache("pets", theCache) // Compiles - this is correct

export function getCachedItems<K extends keyof Cache>(key: K): Cache[K][number][]  {
    return readCache(key, theCache) // ok
}

export function getCachedItemsAndMap<K extends keyof Cache>(key: K)  {
    return readCache(key, theCache)
        .map(o => ({  // map works fine
            id: o.id, // we can even access common members
            item: o
        }));
}

Редактировать

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

type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }

export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}


function readCache<K extends keyof C, C extends { [key in K]: Map<any> }>(
    key: K, cache: C
): Map<C[K][string]> {
    return cache[key]
}

const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct

export function getCachedItems<K extends keyof Cache>(key: K): Map<Cache[K][string]>  {
    return readCache(key, theCache) // ok
}

export function getCachedItemsAndMap<K extends keyof Cache>(key: K)  {
    let cache: Map<Cache[K][string]> = readCache(key, theCache)
}

Редактировать v2

На самом деле я не думаю, что нужно целое, поскольку в Map, к которому вы хотите получить доступ, нет соответствующих методов. Это также работает:

type Pet = { name: string, petFood: string }
type Human = { firstName: string, lastName: string }

export type Map<T> = { [key: string]: T }
export type Cache = { pets: Map<Pet>, humans: Map<Human> }
const theCache: Cache = {pets: {}, humans: {}}


function readCache<K extends keyof C, C extends { [key in K]: Map<any> }>(
    key: K, cache: C
): C[K] {
    return cache[key]
}

const pets: Map<Pet> = readCache("pets", theCache) // Compiles - this is correct

export function getCachedItems<K extends keyof Cache>(key: K): Cache[K]  {
    return readCache(key, theCache) // ok
}

export function getCachedItemsAndMap<K extends keyof Cache>(key: K)  {
    let cache: Cache[K] = readCache(key, theCache)
    let a = cache[''] // Pet | Human
}
...