I want to implement a pick option combined with some key remapping. Something like this:
let demo: {
one$: number
two$: string
}
let result = pick(demo, ['one'])
// result equals { one: number }
So basically I want to pick the object keys without the $
suffix.
By basic pick without key remap is this:
function pick<T, K extends keyof T>(x: T, keys: K[]): Pick<T, K> {
// ...
}
And I can add the suffix with this:
type WithStreamSuffix<T> = { [P in keyof T & string as `${P}$`]: T[P] }
But I have not managed to mix both. Probably it would be easier using a key remap that drops the suffix, but I do not know how.
CodePudding user response:
In order to get type inference to work the way you want for callers of pick()
, you probably need to have K
be the type of keys with the final "$"
already removed, and then add the "$"
back on at the end when doing Pick<T, ...>
. Something like this for the typing:
type DropDollar<T> = T extends `${infer F}\$` ? F : never;
function pick<T, K extends DropDollar<keyof T>>(
x: T, keys: K[]): Pick<T, Extract<`${K}\$`, keyof T>>;
The DropDollar<T>
type uses conditional type inference with template literal types to get everything before the final "$"
. Then when we do Pick
, we need to add it back on like `${K}\$`
. Note that the compiler has no ability to understand the higher order type constraint like `${DropDollar<keyof T>}\$` extends keyof T
for unspecified generic T
. If you just write Pick<T, `${K}\$`>
you will get an error. Instead you need to convince it of this by writing Extract<`${K}\$`,keyof T>
. The Extract<T, U>
utility type can often be used this way you want to use T
in a place that expects U
. If the compiler doesn't see that T extends U
, it will definitely see that Extract<T, U> extends U
.
Anyway then the implementation would maybe be:
function pick(x: any, keys: string[]) {
return keys.reduce((ret, k) => (ret[k "$"] = x[k "$"], ret), {} as any);
}
I'm using a single-call-signature function overload here to separate the concerns of strong typing for the caller and typing for the implementer. The compiler will, again, not be able to follow the correctness of the implementation in terms of unspecified generic T
and K
types, so you'd just get errors otherwise. Loosening the implementation typing to any
and string[]
makes it compile without error. It's not particularly type safe, so you need to double check that you wrote it correctly. But if you did write it correctly then callers benefit from strong typing.
Now you can verify that it behaves as desired:
let demo = { one$: 1, two$: "two" };
let result = pick(demo, ['one']);
console.log(result); // {one$: 1}
Looks good.