简体   繁体   English

打字稿:提供对未从模块导出的变量的类型安全访问的通用函数

[英]Typescript: generic function to provide typesafe access to variable not exported from module

I have a module with an object containing a cache of items of different types, each stored in a property of the cache object:我有一个包含不同类型项目缓存的对象的模块,每个项目都存储在缓存对象的一个​​属性中:

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

I have a function to retrieve data from the cache.我有一个从缓存中检索数据的函数。 It takes in a key, representing a property of the cache (ie pets or humans ), and a generic type specifying the type of data I expect that property to return (ie Pet or Human ).它接受一个键,表示缓存的一个属性(即petshumans ),以及指定我期望该属性返回的数据类型的泛型类型(即PetHuman )。 I can use conditional types to achieve type safety here:我可以在这里使用条件类型来实现类型安全:

// 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

Within the module that all works fine, as I can pass theCache into the readCache function.在模块中一切正常,因为我可以将theCache传递给readCache函数。 However, theCache is not exported as I want to keep it private to the module.但是, theCache没有导出,因为我想将它保留给模块私有。 Instead, I want to export a function that other modules can call to get read-only access to data in the cache, eg something like this:相反,我想导出一个其他模块可以调用的函数来获得对缓存中数据的只读访问,例如这样的:

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

The problem is that the code above doesn't compile:问题是上面的代码不能编译:

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

Is there a way I can get the code to compile (without having to do an unsafe cast)?有没有办法让我编译代码(而不必进行不安全的转换)?

UPDATE : Titian Cernicova-Dragomir made two great suggestions:更新:Titian Cernicova-Dragomir 提出了两个很好的建议:

  1. Return Cache[K] .返回Cache[K] This works fine for places where I want to call the method and know the type I'm working with.这适用于我想调用该方法并知道我正在使用的类型的地方。 But in cases where I have another generic function which wants to call the generic getCachedItems , and know that what it gets back is an array of T , that won't work.但是如果我有另一个通用函数想要调用通用getCachedItems ,并且知道它返回的是一个T数组,那将不起作用。
  2. Return Cache[K][number][] .返回Cache[K][number][] This solves the above problem (and answers the original question).这解决了上述问题(并回答了原始问题)。 But this only works when the cache contains arrays.但这仅在缓存包含数组时有效。 What if I want the cache object to be slightly different (more like a Dictionary or Map in other languages): each entry in the cache should itself be another object, with a property for each item which it caches, where the name of the property is some ID of that object, as suggested when designing a Redux store .如果我希望缓存对象略有不同(更像是其他语言中的字典或地图)怎么办:缓存中的每个条目本身应该是另一个对象,它缓存的每个项目都有一个属性,其中属性的名称是该对象的某个 ID, 正如在设计 Redux store 时所建议的那样

In other words I'd have this:换句话说,我有这个:

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
}

So I still get the original problem that way.所以我仍然以这种方式解决原始问题。 If I try the suggestion of returning C[K] , again, it works if I know the type when calling it, but not in a generic function:如果我再次尝试返回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
}

Conditional types that still contain unresolved type parameters are usually aa problem as the compiler can't expand them.仍然包含未解析类型参数的条件类型通常是一个问题,因为编译器无法扩展它们。

If cache only contains relevant keys an approach that works better is to use index type queries, they produce the same results in this case but are more friendly to the compiler.如果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
}

While the answer above works, the request in the comments is to be able to treat the result of readCache as an array.虽然上面的答案有效,但评论中的请求是能够将readCache的结果视为数组。 While Cache[K] is a union of arrays, its methods are mangled and hard to use.虽然Cache[K]是数组的联合,但它的方法被破坏并且难以使用。 We can drill down one more level and get the item type from Cache[K] using Cache[K][number] and use this as the array item in the result.我们可以再深入一层,使用Cache[K][number]Cache[K]获取项目类型,并将其用作结果中的数组项目。 This will make arrays work well in methods like getCachedItems :这将使数组在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
        }));
}

Edit编辑

Version with the Map type added to the question: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)
}

Edit v2编辑 v2

Actually I don't think the whole is needed since there are no relevant methods on the Map you want to access.实际上,我认为不需要全部,因为您要访问的Map上没有相关方法。 This also works:这也有效:

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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM