简体   繁体   English

Typescript map object 中的特定键

[英]Typescript map specific keys in object

I'm new to typescript and want to write a pretty simple generic function - it gets an object (possibly partial), gets a list of keys and maps the specified keys (if they exist) to a different value, the rest of the values need to stay the same. I'm new to typescript and want to write a pretty simple generic function - it gets an object (possibly partial), gets a list of keys and maps the specified keys (if they exist) to a different value, the rest of the values需要保持不变。 I want all that to be type safe.我希望所有这些都是类型安全的。

So in case I have a type:所以如果我有一个类型:

interface User {
  userID: string;
  displayName: string;
  email: string;
  photoURL: string;
}

I want to be able to have a function mapper which will take an object of type Partial<User> and then a list of keys, like "displayName" | "photoURL"我希望能够拥有一个 function mapper ,它将采用Partial<User>类型的 object 然后是一个键列表,例如"displayName" | "photoURL" "displayName" | "photoURL" and, for instance, capitalize their property values, and leave the rest of the properties untouched. "displayName" | "photoURL" ,例如,将它们的属性值大写,并且保持属性的 rest 不变。

Here are some examples of input/output:以下是输入/输出的一些示例:

// INPUT
const fullUser: User = {
    userID: "aaa",
    displayName: "bbb",
    email: "ccc@ddd.com",
    photoURL: "https://eee.fff",
}

const partialUser = {
    userID: "ggg",
    photoURL: "https://hhh.iii",
}

// OUTPUT
const o1 = capsMapper<User, "displayName" | "userID">(fullUser);
const o2 = capsMapper<User, "displayName" | "userID">(partialUser);

// o1 should be
{
    userID: "AAA",
    displayName: "BBB",
    email: "ccc@ddd.com",
    photoURL: "https://eee.fff",
}

// o2 should be
{
    userID: "GGG",
    photoURL: "https://hhh.iii",
}

So I wrote this simple generic function:所以我写了这个简单的通用 function:

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>) => {
  ???
}

and was hoping to call it like mapper<User, "displayName" | "photoURL">(myObject)并希望将其称为mapper<User, "displayName" | "photoURL">(myObject) mapper<User, "displayName" | "photoURL">(myObject) but I cannot find a way to implement the function body. mapper<User, "displayName" | "photoURL">(myObject)但我找不到实现 function 主体的方法。 The problem is, when I'm iterating over the keys of the passed object, I cannot find a way to check if the key is amongst the Keys type passed as a generic type .问题是,当我遍历传递的 object 的键时,我找不到检查键是否在作为泛型类型传递的Keys类型中的方法 I also tried to have an additional parameter in the function:我还尝试在 function 中添加一个附加参数:

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>, keys: Keys) => {

or even甚至

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>, keys: Keys[]) => {

but still checking the key of the passed (possibly partial) object results in compiler errors.但仍然检查传递的(可能是部分的)object 的密钥会导致编译器错误。

Is it possible to implement such a function?是否可以实现这样的function?

function mapperArray<T, FilteredProp extends keyof T>(obj: Partial<T>, keyList: Array<keyof T>) {
    const a:any = {};
    (new Set(keyList.filter(key => key in obj))).forEach(key => a[key] = obj[key])
    return a as Pick<T, FilteredProp>;
}
const a = {
    prop1: "dshfyuhe",
    prop2: 154
};
const b = mapperArray<typeof a, 'prop2'>(a, ['prop2']);
console.log(b.prop2);

this is closest i could come up with.这是我能想到的最接近的。 I hope this meet requirement, i also know that this can be improved further but currently i doesn't have knowledge and time.我希望这符合要求,我也知道这可以进一步改进,但目前我没有知识和时间。

I could not write it in comment with correct formatting so pasting here.我无法以正确的格式将其写在评论中,因此粘贴在这里。

You definitely need to pass in actual key string arguments into the capsMapper() function.您肯定需要将实际的密钥字符串 arguments 传递到capsMapper() function 中。 The TypeScript compiler is happy to deal with generic type parameters like K extends keyof T , but the static type system, including generics, is erased when TypeScript is emitted to JavaScript, which is what actually runs at runtime. The TypeScript compiler is happy to deal with generic type parameters like K extends keyof T , but the static type system, including generics, is erased when TypeScript is emitted to JavaScript, which is what actually runs at runtime. If you don't have any values of type K , then you're probably not going to be able to do anything with them at runtime.如果您没有任何K类型的,那么您可能无法在运行时对它们执行任何操作。 So instead of <T, K extends keyof T>(data: T) =>... we'll need something like <T, K extends keyof T>(data: T, ...keys: K[]) =>... .所以代替<T, K extends keyof T>(data: T) =>...我们需要类似<T, K extends keyof T>(data: T, ...keys: K[]) =>...

Here's one possible implementation of capsMapper :这是capsMapper的一种可能实现:

const capsMapper = <T extends Partial<Record<K, string>>, K extends keyof T>(
  data: T, ...keys: K[]
) => {
  const d: Omit<T, K> = data;
  const mapped: { [P in K]?: string } = {};
  for (const k of keys) {
    const v = data[k];
    if (typeof v === "string") {
      mapped[k] = v.toUpperCase();
    }
  }

  type WidenString<T> = T extends string ? string : T;
  return { ...d, ...mapped } as { 
    [P in keyof T]: P extends K ? WidenString<T[P]> : T[P] 
  }; 
}

This might be overkill in terms of the typings, but I'll explain it.就打字而言,这可能有点矫枉过正,但我会解释一下。 We only accept as data an object of type T whose keys of type K have string or undefined values.我们只接受T类型的 object 作为data ,其K类型的键具有stringundefined的值。 Then we take data and widen its type from T to Omit<T, K> ... this is just saying "let's ignore any keys of data that are in K " because we're going to overwrite these anyway.然后我们获取data并将其类型从T扩展为Omit<T, K> ...这只是说“让我们忽略K中的任何data键”,因为无论如何我们都会覆盖这些。 We assign this to d for ease of use.为了便于使用,我们将其分配给d

Then we make an object for just the mapped properties called mapped , whose type is essentially Partial<Record<K, string>> , and for each k in the keys array, we captialize any string properties we find and put it into mapped at the same key.然后我们为mapped的属性创建一个keys ,它的类型基本上是mapped Partial<Record<K, string>> k同一把钥匙。

Finally we spread d and mapped into a new object and return it.最后我们d展开并mapped成一个新的 object 并返回。 The type of that output would be naturally seen as Omit<T, K> & Partial<Record<K, string>> , which is correct, but not as specific as you might like. output 的类型自然会被视为Omit<T, K> & Partial<Record<K, string>> ,这是正确的,但不像您希望的那样具体。 That's because every key in keys would be seen as possibly missing, even if you know for a fact that it existed in data .这是因为 keys 中的每个keys都可能被视为丢失,即使您知道它存在于data中也是如此。

To make it more specific, we need a type assertion to tell the compiler that it is actually of type { [P in keyof T]: P extends K? WidenString<T[P]>: T[P] }为了使其更具体,我们需要一个类型断言来告诉编译器它实际上是类型{ [P in keyof T]: P extends K? WidenString<T[P]>: T[P] } { [P in keyof T]: P extends K? WidenString<T[P]>: T[P] } , a mapped type very much like T (the type of data ) except that any mapped string properties are widened to string via the WidenString<T> conditional type . { [P in keyof T]: P extends K? WidenString<T[P]>: T[P] } ,一个映射类型,非常类似于Tdata的类型),除了任何映射的字符串属性都通过WidenString<T> 条件类型扩展为string This is important in the case that your original T type had properties of string literal type ;如果您的原始T类型具有字符串文字类型的属性,这一点很重要; if you uppercase those, you will not get the same type out.如果你将它们大写,你将不会得到相同的类型。

Here let's test it and I'll show the possible need for that later:在这里让我们对其进行测试,稍后我将展示可能的需求:

const fullUser: User = {
  userID: "aaa",
  displayName: "bbb",
  email: "ccc@ddd.com",
  photoURL: "https://eee.fff",
}

const partialUser: Partial<User> = {
  userID: "ggg",
  photoURL: "https://hhh.iii",
}

const o1 = capsMapper(fullUser, "displayName", "userID");
/* const o1: {
    userID: string;
    displayName: string;
    email: string;
    photoURL: string;
} */
console.log(o1)
/* { "userID": "AAA", "displayName": "BBB",
  "email": "ccc@ddd.com", "photoURL": "https://eee.fff" }  */

const o2 = capsMapper(partialUser, "displayName", "userID");
/* const o2: {
    userID?: string | undefined;
    displayName?: string | undefined;
    email?: string | undefined;
    photoURL?: string | undefined;
} */

console.log(o2);
/*  { "userID": "GGG", "photoURL": "https://hhh.iii" } */

That looks reasonable, I think.我认为这看起来很合理。 The output looks right, and the types are the same... o1 is still a User , and o2 is still a Partial<User> . output 看起来不错,类型相同... o1仍然是Usero2仍然是Partial<User> In the case where we have literal types, though, this happens:但是,在我们有文字类型的情况下,会发生这种情况:

interface Foo {
  a?: "x" | "y"
}
const foo: Foo = { a: "x" };
const o3 = capsMapper(foo, "a");
/* const o3: {
    a?: string | undefined; // this is not "x" | "y" anymore, so o3 is not a Foo
} */
console.log(o3)
// {"a": "X"}

Here o3 has an a property which is "X" in uppercase.这里o3a属性,它是大写的"X" That means o3 is not a valid Foo , whose a property can only be "x" or "y" in lowercase.这意味着o3不是有效的Fooa属性只能是小写的"x""y" And that's why I have that WidenString<T> type in there.这就是为什么我在那里有WidenString<T>类型。 It turns the input type {a?: "x" | "y"}它将输入类型{a?: "x" | "y"} {a?: "x" | "y"} into the output type {a?: string} . {a?: "x" | "y"}进入 output 类型{a?: string}

If you don't expect this kind of thing to happen, you could make the typing simpler by writing instead:如果您不希望发生这种事情,您可以通过编写来简化输入:

return { ...d, ...mapped } as T;

and then o3 would be claimed by the compiler to be of type Foo when it isn't.然后编译器将o3声明为Foo类型,而实际上它不是。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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