การเปลี่ยนชื่อคุณสมบัติในชนิดที่แมปของ 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 ไปยังโค้ด
