Let's say I have an object:
const a = {
section_title: 'Hello world',
section_desc: 'Lorem ipsum dolor sit amet'
}
and a utility that finds all properties by a 'prefix' and returns a new object with those properties without the 'prefix' (i.e. section_title -> title)
function findPropsByPrefix(data, prefix) {
return Object.keys(data)
.filter((key) => key.startsWith(prefix))
.reduce((prev, curr) => {
prev[curr.substr(prefix.length)] = data[curr]
return prev
}, {})
}
// Convert { section_title: '...', section_desc: '...' }
// into { title: '...', desc: '...' }
const b = findPropsByPrefix(a, 'section_')
How should I type the utility function findPropsByPrefix
?
In this Typescript playground, I'm trying using Record<string, any>
:
function findPropsByPrefix(data: Record<string, any>, prefix: string): Record<string, any> {
...
}
but this leads to the error Type 'Record<string, any>' is missing the following properties from type 'B': title, desc
when I assign the returned value to b
.
CodePudding user response:
The call signature for extractFieldsByPrefix()
should be something like this:
// call signature
function extractFieldsByPrefix<P extends string, T extends Record<string, any>>(
data: T, prefix: P): { [K in keyof T as K extends `${P}${infer R}` ? R : never]: T[K] };
In order to keep track of the specific keys and values, the function is generic in both P
, the type of prefix
, and T
, the type of data
. The return type is computed from T
and P
using template literal types and key remapping. Let's examine it:
{ [K in keyof T as K extends `${P}${infer R}` ? R : never]: T[K] }
This is a mapped type which looks at each key K
from the keys of T
. For each such key, it uses a conditional type (K extends ... ? ... : ...
) to try to see if that key starts with the prefix P
and, if so, use conditional type inference to infer
the suffix R
(`${P}${infer R}`
is a template literal type representing a string you get when concatenating P
to R
for some R
). This works because template literal types support such inference. If the inference succeeds, then we remap the old key K
to the new key R
(so just the suffix). If the inference fails, then we remap to old K
key to never
, which has the effect of dropping the property entirely. The property value type is just T[K]
, so the new property at R
will have the same type as the old property at K
.
It's not generally possible for the compiler to verify that a function implementation satisfies a generic call signature with conditional or remapped type output; see microsoft/TypeScript#33912 for a related issue. For now the best thing to do is to use something like a single-call-signature overload so that the implementation is somewhat loosely checked:
// implementation
function extractFieldsByPrefix(data: any, prefix: string) {
return Object.keys(data)
.filter((key) => key.startsWith(prefix))
.reduce<any>((prev, curr) => {
prev[curr.substring(prefix.length)] = data[curr]
return prev
}, {})
}
By giving both data
and the result of reduce()
the any
type, I'm essentially saying I don't want the compiler to complain about any possible type problems. That is the easiest thing to do, but it does mean that you need to be careful with that implementation, since a lack of compiler error does not mean the implementation is bug-free. If you, for example, wrote endsWith()
instead of startsWith()
, the compiler still wouldn't complain, but now the output at runtime would not conform to the call signature's return type. So be careful.
Anyway, let's see if it works:
const result = extractFieldsByPrefix(a, 'section_');
/* const result: {
title: string;
desc: string;
} */
console.log(result.title.toUpperCase()) // HELLO WORLD!
const b: B = result; // okay
console.log(b)
/* {
"title": "Hello world!",
"desc": "Lorem ipsum dolor sit amet"
} */
// make sure that dropping fields without the prefix also happens
const also = extractFieldsByPrefix(
{ who: 1, what: 2, when: 3, where: 4, why: 5, how: 6 },
"w"
);
/* const also: {
ho: number;
hat: number;
hen: number;
here: number;
hy: number;
} */
console.log(also);
/* {
"ho": 1,
"hat": 2,
"hen": 3,
"here": 4,
"hy": 5
} */
Looks good! Playground link to code