Issue
I am typing a query engine of sorts. Here's what things look like so far:
type WhereClause = { [key: string]: string | number | boolean };
type $Option = { "$"?: { where: WhereClause } };
type Subquery = { [namespace: string]: NamespaceVal };
type NamespaceVal = $Option & Subquery | $Option;
type Query = { [namespace: string]: NamespaceVal };
type InstantObject = {
id: string;
[prop: string]: any;
};
type Responsify<T> = {
[P in keyof T]: T[P] extends object
? (Responsify<Omit<T[P], "$">> & { id: string; [k: string]: any })[]
: T[P][];
};
function queryResponse<T extends Query>(q: T): Responsify<T> {
return 1 as any
}
const resOne = queryResponse({users: {$: {where: {"foo": 1}}, comments: {}}});
This works, but the output type is quite verbose:
declare const resOne: Responsify<{
users: {
$: {
where: {
foo: number;
};
};
comments: {};
};
}>;
This can make it hard for end users to read through intellisense.
Question:
Is there some way, where I could produce a "cleaner" looking output type?
A clear win, would be if I could omit the $ altogether:
declare const resOne: Responsify<{
users: {
comments: {};
};
}>;
Solution
So you want IntelliSense to take a type like Responsify<T> and display it as Responsify<{⋯}> where the type inside is some version of T with any properties named "$" starting at the second level down removed. (I say second level because your original definition of Responsify<T> wouldn't remove a top-level property with a key of "$". Not sure if that matters, but it's a little wrinkle.)
You can't really do this; it would be hard to imagine writing Responsify<X> and having the compiler somehow evaluate it to display Responsify<Y> for some other type Y. That would mean presumably that sometimes Responsify<T> does expand the definition and other times it doesn't. And I don't think this is possible. Or at least I haven't found a way.
The closest thing I can imagine is to define Responsify<T> in terms of some other type named, say, _Responsify<T>, so that the former will always display the latter. And so that means I want this:
type Responsify<T> = _Responsify<{ [K in keyof T]: DeDollar<T[K]> }>
So here _Responsify<T> is more or less your original definition, and DeDollar<T> is a type that recursively removes the "$" key from all levels. We could write that like this:
type DeDollar<T> =
T extends readonly any[] ? { [I in keyof T]: DeDollar<T[I]> } :
T extends object ? { [K in keyof T as Exclude<K, "$">]: DeDollar<T[K]> } :
T
This is very similar to using the Omit utility type but by writing out {[K in keyof T as Exclude<K, "$">]: ⋯} we avoid seeing the word Omit in the type display.
That's mostly it. Of course if you only call Responsify on types that have already been DeDollared, then _Responsify doesn't have to worry about "$" anymore:
type _Responsify<T> = {
[P in keyof T]: T[P] extends object
? (_Responsify<T[P]> & { id: string;[k: string]: any })[]
: T[P][];
};
That shouldn't change anything. Okay, let's test it out:
const resOne = queryResponse({ users: { $: { where: { "foo": 1 } }, comments: {} } });
// ^? const resOne: _Responsify<{ users: { comments: {}; }; }>
That's what you wanted. And you can verify that this type expands to the same as your original version, possibly by using something like ExpandRecursively as described at How can I see the full expanded contract of a Typescript type?:
type ResOne = ExpandRecursively<typeof resOne>;
/* type ResOne = {
users: {
[x: string]: any;
comments: {
[x: string]: any;
id: string;
}[];
id: string;
}[];
} */
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.