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 DeDollar
ed, 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.