ค้นหาเว็บไซต์

การเปลี่ยนชื่อคุณสมบัติในชนิดที่แมปของ Typescript


ฉันมีคอลเลกชันของอ็อบเจ็กต์ Typescript ที่ดูเหมือนแบบนี้:

SomeData {
  prop1: string;
  prop2: number;
}

และฉันจำเป็นต้องลงท้ายด้วยวัตถุบางอย่างที่มีหน้าตาแบบนี้:

type SomeMoreData= {
  prop1Change: string;
  prop2Change: number;
}

ฉันรู้ว่าถ้าฉันแค่ต้องการเปลี่ยนประเภท ฉันก็สามารถทำแบบนี้ได้:

type SomeMoreData<T> = {
  [P in keyof T]: boolean;
}

ซึ่งจะให้ฉัน:

SomeMoreData<SomeData> {
  prop1: boolean;
  prop2: boolean;
}

มีวิธีใดที่จะจัดการชื่อที่สร้างโดยส่วน [P in keyof T] ไหม? ฉันได้ลองสิ่งต่าง ๆ เช่น [(P in keyof T) + "Change"] แต่ก็ไม่สำเร็จ

คำตอบ #1

การแมปคีย์ใหม่ใน Mapped Types ถูกแนะนำใน TypeScript 4.1:

type SomeData = {
    prop1: string;
    prop2: number;
}

type WithChange<T> = { [P in keyof T & string as `${P}Change`]: T[P] };

type SomeMoreData = WithChange<SomeData>; // {  prop1Change: string, prop2Change: number }

สนามเด็กเล่น


ในตัวอย่างข้างต้น เราใช้คุณสมบัติใหม่สองอย่าง: as clause ใน mapped types + template literal type

ชนิดของสตริงแบบเทมเพลตคือพื้นที่ชนิดที่เทียบเท่ากับนิพจน์สตริงแบบเทมเพลต คล้ายกับนิพจน์สตริงแบบเทมเพลต ชนิดสตริงแบบเทมเพลตถูกล้อมรอบด้วยตัวแบ่ง backtick และสามารถมีตัวแปรแทนที่ในรูปแบบ $ {T} ซึ่ง T คือชนิดที่สามารถกำหนดค่าให้กับ string, number, boolean, หรือ bigint ชนิดสตริงแบบเทมเพลตให้ความสามารถในการรวมสตริงตัวอักษร แปลงค่าตัวอักษรจากชนิดพื้นฐานที่ไม่ใช่สตริงเป็นตัวแทนของสตริง และเปลี่ยนตัวพิมพ์ใหญ่หรือตัวพิมพ์เล็กของตัวอักษร นอกจากนี้ ผ่านการอนุมานชนิด ชนิดสตริงแบบเทมเพลตยังให้รูปแบบง่ายๆ ของการจับคู่และการแยกสตริง

ตัวอย่างบางส่วน:

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
type T0 = EventName<'foo'>;  // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'
type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

นอกจากนี้ ประเภทที่แมปยังรองรับข้อ as แบบไม่บังคับ ซึ่งสามารถระบุการแปลงชื่อคุณสมบัติที่สร้างขึ้นได้:

{ [P in K as N]: X }

โดยที่ N ต้องเป็นประเภทที่สามารถกำหนดค่าให้กับ string ได้

คำตอบ #2

คำตอบก่อน TS4.1;

คุณไม่สามารถทำมันโดยอัตโนมัติ อุปสรรคใหญ่คือในปัจจุบันไม่มีตัวดำเนินการประเภทที่ให้คุณต่อข้อความตัวอักษรในระดับประเภทได้ ดังนั้นคุณจึงไม่สามารถอธิบายการแปลงที่คุณกำลังทำได้

// without Append<A extends string, B extends string>, you can't type this:

function appendChange<T extends string>(originalKey: T): Append<T,'Change'> {
  return originalKey+'Change';
}

มีข้อเสนอสำหรับฟีเจอร์นี้ แต่ใครจะรู้ล่ะว่าจะเกิดขึ้นจริงหรือไม่

นั่นหมายความว่าถ้าคุณต้องการเปลี่ยนแปลงคีย์ คุณจำเป็นต้องเขียนแมปปิ้งเฉพาะที่คุณกำลังมองหาแบบโค้ดลงไปโดยตรง จากสตริงตัวอักษรไปยังสตริงตัวอักษร:

type SomeMoreDataMapping = {
  prop1: "prop1Change"
  prop2: "prop2Change"
}

ด้วยการทำแผนที่นี้ คุณสามารถกำหนดสิ่งเหล่านี้ได้:

type ValueOf<T> = T[keyof T]
type KeyValueTupleToObject<T extends [keyof any, any]> = {
  [K in T[0]]: Extract<T, [K, any]>[1]
}
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in keyof T]: [K extends keyof M ? M[K] : K, T[K]]
  }>>

สรุปสั้น ๆ:

  • ValueOf<T> เพียงแค่ส่งค่าของชนิดรวมของค่าของคุณสมบัติสำหรับชนิด T.
  • KeyValueTupleToObject จะรับ union ของประเภททูเพิลแบบนี้: ["a",string] | ["b",number] และแปลงให้เป็นประเภทอ็อบเจ็กต์แบบนี้: {a: string, b: number}.
  • และ MapKeys<T, M> ใช้ชนิดข้อมูล T และการแมปคีย์ M แล้วแทนที่คีย์ใด ๆ ใน T ที่มีอยู่ใน M ด้วยคีย์ที่สอดคล้องจาก M หากคีย์ใน T ไม่มีอยู่ใน M คีย์นั้นจะไม่ถูกเปลี่ยนแปลง หากคีย์ใน M ไม่มีอยู่ใน T คีย์นั้นจะถูกละเว้น

ตอนนี้คุณสามารถทำสิ่งนี้ (ในที่สุด) ได้แล้ว:

type SomeMoreData= MapKeys<SomeData, SomeMoreDataMapping>;

และถ้าคุณตรวจสอบ SomeMoreData คุณจะเห็นว่ามีชนิดที่ถูกต้อง:

var someMoreData: SomeMoreData = {
  prop1Change: 'Mystery Science Theater',
  prop2Change: 3000
} // type checks

นี่ควรจะช่วยให้คุณทำสิ่งสนุกๆ ได้ เช่น:

function makeTheChange<T>(input: T): MapKeys<T, SomeMoreDataMapping> {
  var ret = {} as MapKeys<T, SomeMoreDataMapping>;
  for (var k in input) {
    // lots of any needed here; hard to convince the type system you're doing the right thing
    var nk: keyof typeof ret = <any>((k === 'prop1') ? 'prop1Change' : (k === 'prop2') ? 'prop2Change' : k);
    ret[nk] = <any>input[k];    
  }
  return ret;
}

var changed = makeTheChange({ prop1: 'Gypsy', prop2: 'Tom', prop3: 'Crow' });
console.log(changed.prop1Change.charAt(0)); //ok
console.log(changed.prop2Change.charAt(0)); //ok
console.log(changed.prop3.charAt(0)); //ok

หวังว่านั่นจะช่วยได้นะ โชคดี!

ปรับปรุงล่าสุดกันยายน 2018 เพื่อใช้ประโยชน์จากประเภทตามเงื่อนไขที่แนะนำกับ TS 2.8


อีกหนึ่งการอัปเดต: ถ้าคุณต้องการให้สิ่งนี้ทำงานกับคุณสมบัติที่เป็นตัวเลือก มันจะซับซ้อนขึ้น:

type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in RequiredKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>> & Partial<KeyValueTupleToObject<ValueOf<{
    [K in OptionalKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>>> extends infer O ? { [K in keyof O]: O[K] } : never;

ถ้าคุณต้องรองรับดัชนีลายเซ็น มันจะซับซ้อนยิ่งขึ้นไปอีก

ลิงก์ Playground ไปยังโค้ด

บทความที่เกี่ยวข้อง