typescript: สร้าง tuple [key,value type] จาก interface
ฉันกำลังพยายามสร้างประเภท tuple ใน TypeScript (คู่ค่า) ที่สามารถเก็บค่าได้เฉพาะที่ค่าตัวแรกเป็น keyof ของ interface บางตัวซึ่งเก็บการดำเนินการที่ถูกต้องเป็นคีย์ และชนิดของอาร์กิวเมนต์ที่คาดหวังเป็นค่า
ฉันคิดว่ารหัสด้านล่างนี้จะอธิบายได้ดีกว่าสิ่งที่ฉันพยายามทำให้สำเร็จ:
interface IEventsAndArguments {
'+': number,
'-': number,
'change_name': string,
}
// Now I want to produce tuples,
// where the first value is in keyof IEventsAndArguments
// and the second value is of the corresponding type
// (but a type with 2 properties name and value would do as well)
// it works for function arguments
function myFunction<K extends keyof IEventsAndArguments>(x:K, y:IEventsAndArguments[K]) {}
myFunction('+', 'a string'); // invalid as expected, '+' needs a number
myFunction('+', 4);
myFunction('change_name', 'John');
function myFunction2<K extends keyof IEventsAndArguments>({x, y}:{x:K, y:IEventsAndArguments[K]}) {}
myFunction2({x: '+', y: 'another string'}); // invalid as expected
myFunction2({x: '-', y: 3});
// it doesn't work when I try to define a tuple type
type TTuple<K extends keyof IEventsAndArguments> = [K, IEventsAndArguments[K]];
// I would like the next tuple to be INVALID
// but it is valid since the second element is of type number | string
const tuple1:TTuple<keyof IEventsAndArguments> = [ '+', 'hello' ]
// What I ultimately would want is something similar to this
type TTuple2<K extends keyof IEventsAndArguments> = [
K,
(v:IEventsAndArguments[K]) => void
];
// I would like the next tuple to be VALID but it is invalid
// since the function argument must be of type number | string
const tuple2:TTuple2<keyof IEventsAndArguments> = [
'+',
(t:number) => {},
];
นี่คือลิงก์สนามเด็กเล่นที่มีโค้ดเดียวกัน
มีวิธีใดที่จะทำให้สิ่งนี้ใช้งานได้หรือไม่? มีใครอธิบายได้ไหมว่าทำไมมันถึงทำงานตามที่ฉันต้องการกับฟังก์ชัน แต่ไม่เมื่อกำหนดประเภทใหม่?
ขอบคุณ
แก้ไข: ฉันคิดว่าฉันเจอกลไกที่ 'พอใช่' แล้ว: เนื่องจากมันใช้ได้กับฟังก์ชัน เราสามารถใช้ฟังก์ชันเพื่อสร้างทูเพิลที่ถูกต้องได้!
// if we use a function to produce the tuple, we can produce 'valid'
// tuples, even if the tuple type itself is not as strict as we'd like...
function tuple<K extends keyof IEventsAndArguments>(
a:K, f:(v:IEventsAndArguments[K]) => void
):TTuple2<K>
{
return [a, f];
}
const tuple3 = tuple('+', (v:number) => v + 1); // valid, as expected
const tuple4 = tuple('+', (v:string) => v + 1); // invalid, also as expected
แต่ความหงุดหงิดยังคงอยู่: ทำไมมันถึงใช้ได้กับฟังก์ชัน (ทั้งพารามิเตอร์ 'เชื่อมโยง' กับรายการเดียวกันของอินเทอร์เฟซ) แต่ไม่ใช่ตอนที่สร้างชนิดใหม่? จะดีถ้าเราสามารถทำให้ชนิดเข้มงวดเท่าที่เราต้องการได้ด้วย เพราะตอนนี้คุณยังสามารถสร้างทูเพิล 'ไม่ถูกต้อง' ได้โดยไม่ใช้ฟังก์ชัน
โอ้: สิ่งสำคัญอีกอย่าง: ฉันต้องการให้สิ่งนี้เป็นแบบทั่วไป ดังนั้นไม่ว่าข้อมูลที่คนจะ 'เพิ่ม' เข้าไปในอินเทอร์เฟซในภายหลัง มันก็ควรทำงานต่อไปได้ ดังนั้นควรถือว่าการนิยามของ IEventsAndArguments เป็นสิ่งที่ไม่ทราบและสามารถขยายได้ในที่อื่น ๆ
คำตอบ #1
คุณต้องการให้ชนิด TTuple ของคุณ เป็นได้ทั้ง tuple ในรูปแบบ ["+", (v: number) => void], หรือ ในรูปแบบ ["-", (v: number) => void], หรือ ในรูปแบบ ["change_name", (v: string) => void]. คุณสามารถทำให้สิ่งนี้เกิดขึ้นได้โดยไม่ต้องใช้ generics เพียงแค่ทำให้ TTuple เป็น union ของชนิดเหล่านั้น:
type TTuple =
["+", (v: number) => void] |
["-", (v: number) => void] |
["change_name", (v: string) => void];
คุณสามารถตรวจสอบได้ว่าสิ่งนี้ใช้งานได้:
let tuple: TTuple;
tuple = ['+', (t: number) => console.log(t.toFixed())]; // okay
tuple = ['change_name', (t: number) => console.log(t.toFixed())]; // error
tuple = ['change_name', (t: string) => console.log(t.toUpperCase())]; // okay
แน่นอนว่าคุณไม่ต้องการกำหนด TTuple ด้วยตนเอง; จะดีกว่าถ้าปล่อยให้ตัวคอมไพเลอร์คำนวณ TTuple เป็นฟังก์ชันของ IEventsAndArguments
วิธีหนึ่งในการทำเช่นนี้คือการสร้างสิ่งที่เรียกว่า "ชนิดวัตถุแบบแจกแจง" (คำศัพท์มาจาก microsoft/TypeScript#47109) โดยที่คุณเริ่มจากการแมปไปที่ชนิด IEventsAndArguments เพื่อสร้างชนิดวัตถุใหม่ที่มีคีย์เหมือนกัน แต่ค่าของคุณสมบัติเป็นชนิดทูเพิลที่สอดคล้องกัน จากนั้นคุณจะทำการดัชนีทันทีในชนิดที่แมปนั้นด้วยยูเนียนของคีย์ IEventsAndArguments ซึ่งจะได้ผลลัพธ์เป็นยูเนียนของทูเพิล ตัวอย่างการทำงานเป็นดังนี้:
type TTuple = { [K in keyof IEventsAndArguments]: [
K,
(v: IEventsAndArguments[K]) => void
] }[keyof IEventsAndArguments];
การเดินผ่านสิ่งนั้น ประเภทที่ถูกทำแผนที่ {[K in keyof]... } จะวนซ้ำผ่านคีย์ "+", "-", และ "change_name" เพื่อสร้างบางอย่างเช่น
{
"+": ["+", (v: IEventsAndArguments["+"] )=> void],
"-": ["-", (v: IEventsAndArguments["-"] )=> void],
"change_name": ["change_name", (v: IEventsAndArguments["change_name"] )=> void]
}
ซึ่งลดลงเป็น
{
"+": ["+", (v: number) => void],
"-": ["-", (v: number) => void],
"change_name": ["change_name", (v: string) => void]
}
จากนั้นเราจะเข้าดัชนีในนั้นด้วย "+" | "-" | "change_name" ซึ่งจะได้สหภาพ ["+", (v: number) => void] | ["-", (v: number) => void] | ["change_name", (v: string) => void] ตามที่ต้องการ
ตอนนี้หากคุณเพิ่มหรือเปลี่ยนคุณสมบัติในประเภท IEventsAndArguments ประเภท TTuple จะเปลี่ยนโดยอัตโนมัติเพื่อรองรับมัน
ลิงก์ Playground ไปยังโค้ด
