简体   繁体   中英

Typescript dynamic object type definition

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' (ie 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 .

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM