Issue
Cache function should accept a configuration object with up to ten different entities. If entity config has fields property then it should pick this fields from object.
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'>>;
I tried to write something like this, but it works only for the first overload:
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;
Solution
The approach I'd take is something like this:
declare function useCache<T extends Params>(params: T): MyCache<T>;
where we'd have to define Params
as a supertype to which all acceptable inputs to useCache()
are assignable, and we'd also have to define a generic utility type MyCache<T>
which transforms an input type T
constrained to Params
into the expected output type.
First you will definitely need some kind of mapping from the name of the key to the expected type. You've already done this in your example:
interface EntityMap {
users: User,
companies: Company,
}
And from this we can define Params
as a mapped type:
type Params = {
[K in keyof EntityMap]?: {
ids: EntityMap[K]["id"][],
fields?: (keyof EntityMap[K])[]
}
}
Note that this only works because each property type inside EntityMap
has an id
key. Also I didn't assume that the id
would be a string
. Instead I use the indexed access type EntityMap[K]["id"]
to say that whatever the id
property is, you need to use that type in the ids
field.
Now we can write MyCache<T>
as follows:
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]
> };
This is another mapped type, although instead of just mapping over keyof EntityMap
, we map over the intersection of keyof EntityMap
and keyof T
. That way the output type only contains those keys of EntityMap
which are also present as keys of T
.
The property type is Record<string, ⋯>
(using the Record
utility type) where ⋯
depends on whether or not the property T[K]
has a fields
entry or not. If it doesn't, then it's just EntityMap[K]
, the corresponding entity type. If it does, then we use conditional type inference to pull out the element type F
of the fields
array, and constrain it to be a key of EntityMap[K]
, and then return Pick<EntityMap[K], F>
(using the Pick
utility type) instead of just EntityMap[K]
.
We're nearly there, but it might be a good idea to constrain T
in useCache()
just a bit more:
declare function useCache<T extends Params &
{ [K in keyof T as Exclude<K, keyof EntityMap>]: never }>(
params: T): MyCache<T>;
This constraint will check if T
has any extra properties other than those in keyof EntityMap
, and if so, require them to be the impossible never
type. That will discourage the use of any extra keys. Extra keys wouldn't necessarily hurt anything (although this depends on the implementation of useCache()
, and the output of MyCache<T>
would just ignore them, but it's arguably worthy of a compiler error.
Okay, let's test it out:
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">>
Looks good. The basic usage works as desired. Properties with fields
return a Pick
type, while those without return the full entity type. And let's just make sure extra keys are prohibited:
useCache({ oops: 123 }); // error!
// ~~~~
// Type 'number' is not assignable to type 'never'
Also looks good.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.