缓存函数应接受最多具有十个不同实体的配置对象.如果实体配置具有字段属性,则它应从对象中选取此字段.

const { users, companies } = useCache({
    users: {
        ids: ['1', '2'],
    },
    companies: {
        ids: ['3', '4'],
        fields: ['title'],
    }
});

// typeof users = Record<string, User>;
// typeof companies = Record<string, Pick<Company, 'title'>>;

我试着写这样的东西,但它只适用于第一个重载:

type EntityMap = {
    users: User,
    companies: Company,
}

type Keys<T> = Array<keyof T>;

type PartialEntity<T extends object, TFields extends Keys<T> | undefined = undefined> = TFields extends undefined
    ? T
    : Pick<T, NonNullable<TFields>[number]>;

type EntityConfig<TName extends keyof EntityMap, TFields extends Keys<EntityMap[TName]> | undefined> = {
    [K in TName]: {
        ids: string[];
        fields?: TFields;
    }
};

type EntityResult<TName extends keyof EntityMap, TFields extends Keys<EntityMap[TName]> | undefined> = {
    [K in TName]: Record<string, PartialEntity<EntityMap[K], TFields>>;
};

interface CacheFunction {
    <TName1 extends keyof EntityMap, TFields1 extends Keys<EntityMap[TName1]>>(
        params: EntityConfig<TName1, TFields1>
    ): EntityResult<TName1, TFields1>;

    <
     TName1 extends keyof EntityMap,
     TFields1 extends Keys<EntityMap[TName1]> | undefined,
     TName2 extends keyof EntityMap,
     TFields2 extends Keys<EntityMap[TName2]> | undefined
    >(
        params: EntityConfig<TName1, TFields1> & EntityConfig<TName2, TFields2>
    ): EntityResult<TName1, TFields1> & EntityResult<TName2, TFields2>;

    // same up to 10 names
}

declare const useCache: CacheFunction;

ts playground

推荐答案

我会采取这样的方法:

declare function useCache<T extends Params>(params: T): MyCache<T>;    

我们必须将Params定义为一个超类型,useCache()的所有可接受输入都可以赋值给它,我们还必须定义一个generic实用程序类型MyCache<T>,它将输入类型TconstrainedParams转换为预期的输出类型.


首先,您肯定需要从键的名称到预期类型的某种映射.您已经在示例中做到了这一点:

interface EntityMap {
    users: User,
    companies: Company,
}

由此我们可以将Params定义为mapped type:

type Params = {
    [K in keyof EntityMap]?: {
        ids: EntityMap[K]["id"][],
        fields?: (keyof EntityMap[K])[]
    }
}

请注意,这只是因为EntityMap中的每个属性类型都有一个id键. 我也没想到id会是string.相反,我使用indexed access type EntityMap[K]["id"]来说明,无论id属性是什么,您都需要在ids字段中使用该类型.

现在我们可以这样写MyCache<T>个字:

type MyCache<T extends Params> = {
    [K in keyof EntityMap & keyof T]: Record<string,
        T[K] extends {
            fields: (infer F extends keyof EntityMap[K])[]
        } ? Pick<EntityMap[K], F> : EntityMap[K]
    > };

这是另一个映射类型,尽管我们不是只映射keyof EntityMap,而是映射keyof EntityMapkeyof Tintersection.这样,输出类型只包含EntityMap的那些键,它们也作为T的键存在.

属性类型是Record<string, ⋯>(使用the Record utility type),其中取决于属性T[K]是否有fields条目. 如果没有,那么它只是EntityMap[K],对应的实体类型.如果是这样,那么我们使用conditional type inference取出fields数组的元素类型Fconstrain it作为EntityMap[K]的键,然后返回Pick<EntityMap[K], F>(使用the Pick utility type)而不仅仅是EntityMap[K].


我们就快做到了,但把useCache()个人中的T人再限制一点可能是个好主意:

declare function useCache<T extends Params &
    { [K in keyof T as Exclude<K, keyof EntityMap>]: never }>(
        params: T): MyCache<T>;

此约束将判断T是否有extra属性,如果有,则要求它们为the impossible never type. 这将阻止使用任何额外的密钥.额外的键不一定会造成任何伤害(尽管这取决于useCache()的实现,MyCache<T>的输出会忽略它们,但可以说它值得编译器错误.


好的,让我们来测试一下:

const { users: users1 } = useCache({
    users: {
        ids: ['1', '2'],
    },
});
//^? const users1: Record<string, User>
const userName = users1['1'].name; // okay

const { users: users2 } = useCache({
    users: {
        ids: ['1', '2'],
        fields: ['name', 'id']
    }
});
// const users2: Record<string, Pick<User, "id" | "name">>
users2['1'].name // okay
users2['1'].age // error
//          ~~~
// Property 'age' does not exist on type 'Pick<User, "id" | "name">'.

const { users, companies } = useCache({
    users: {
        ids: ['1', '2'],
    },
    companies: {
        ids: ['3', '4'],
        fields: ['title'],
    }
});
// const users: Record<string, User>
// const companies: Record<string, Pick<Company, "title">>

看上go 不错.基本用法可以达到预期效果.具有fields的属性返回Pick类型,而不具有fields的属性返回完整的实体类型.让我们确保禁止额外的密钥:

useCache({ oops: 123 }); // error!
//         ~~~~
// Type 'number' is not assignable to type 'never'

看起来也不错.

Playground link to code

Typescript相关问答推荐

为什么在TypScript中写入typeof someArray[number]有效?

带有联合参数的静态大小数组

Angular 行为:属性类型从SignalFunction更改为布尔

如何在TypeScrip面向对象程序设计中用方法/属性有条件地扩展类

返回同时具有固定键和变量键的对象的函数的返回类型

在嵌套属性中使用Required

PrimeNG日历需要找到覆盖默认Enter键行为的方法

如何将CSV/TXT文件导入到服务中?

通过按键数组拾取对象的关键点

是否可以基于对象的S属性值[]约束对象的其他属性值用于类型化的对象数组?

有没有可能产生输出T,它在视觉上省略了T中的一些键?

函数重载中的不正确TS建议

使用动态输出类型和泛型可重用接口提取对象属性

从子类集合实例化类,同时保持对Typescript中静态成员的访问

不带其他属性的精确函数返回类型

在Mac和Windows上运行的Web应用程序出现这种对齐差异的原因是什么?(ReactNative)

在组件卸载时try 使用useEffect清除状态时发生冲突.react +打字

打字脚本中的模块化或命名空间

基于参数的 TS 返回类型

React HashRouter,单击导航栏时页面重新加载出现问题