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

แทนที่สตริงหลายตัวที่เฉพาะเจาะจงในประเภทของ TypeScript


ฉันกำลังมองหาวิธีการแทนที่คีย์ของประเภทหนึ่งเมื่อมันมีคีย์ของอีกประเภทหนึ่ง แต่ละคีย์จะมีการแทนที่เพียงครั้งเดียว (ในตัวอย่าง 'abc' จะปรากฏในคีย์เพียงครั้งเดียว) ซึ่งจะต้องถูกแมปไปยังค่าที่เทียบเท่าในประเภทการแทนที่

ปัจจุบันฉันกำลังทำ:

type Replacements = {
    abc: 'cba';
    bcd: 'dcb';
};

type Test = {
    bcd: string;
    aabca: string;
};

type ReplaceKeys<R extends Record<string, string>, T extends Record<string, unknown>> = {
    [K in keyof T as K extends keyof R ? R[K] : K]: T[K];
  };
type Replaced = ReplaceKeys<Replacements, Test>;

ซึ่งให้ประเภทนี้กับฉันตามที่คาดไว้:

type Replaced = {
    dcb: string;
    aabca: string;
}

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

type ReplaceKeysInferred<
    R extends Record<string, string>,
    T extends Record<string, unknown>,
> = {
    [K in keyof T as K extends `${infer A}${infer RKey extends keyof R & string}${infer B}`
        ? `${A}${R[RKey]}${B}`
        : K]: T[K];
};

type ReplacedInferred = ReplaceKeysInferred<Replacements, Test>;

อย่างไรก็ตาม ประเภทที่ส่งคืนไม่ได้มีการแทนที่ใด ๆ:

type ReplacedInferred = {
    bcd: string;
    aabca: string;
}

ไม่แน่ใจว่าฉันทำผิดตรงไหน

แก้ไข: ฉันยังได้ลอง:

type ReplaceKeysInferred<
    R extends Record<string, string>,
    T extends Record<string, unknown>,
> = {
    [K in keyof T as K extends `${infer A}${infer RKey}${infer B}`
        ? RKey extends keyof R
            ? `${A}${R[RKey]}${B}`
            : K
        : K]: T[K];
};

ซึ่งโดยพื้นฐานแล้วเป็นขั้นตอนเพิ่มเติมจากประเภทแรก วิธีนี้ทำงานเหมือนกับประเภทแรกโดยให้ผลลัพธ์กลับ:

type ReplacedInferred = {
    bcd: string;
    aabca: string;
}

ซึ่งยังไม่ได้แทนที่สตริงที่อยู่ภายในใด ๆ

คำตอบ #1

แนวทางทั่วไปจะเป็น

type ReplaceKeysInferred<R extends { [k: string]: string }, T extends object> = {
    [K in keyof T as K extends string ? Substitute<R, K> : K]: T[K]
}

ที่ Substitute<R, K> แทนที่รายการที่ตรงกันใด ๆ จาก R ภายในสตริง K วิธีหนึ่งในการใช้งาน Substitute คือ:

type Substitute<R extends { [k: string]: string }, T extends string> =
    OrDefault<{ [K in string & keyof R]:
        T extends `${infer F}${K}${infer L}` ? `${F}${R[K]}${L}` : never
    }[string & keyof R], T>

type OrDefault<T, U> = [T] extends [never] ? U : T;
   

เรากำลังทำการแมปผ่านคีย์แบบสตริงของ R และพยายามจับคู่แต่ละคีย์นั้น K กับซับสตริงบางส่วนของ T ซึ่งจะใช้การอนุมานเทมเพลตลิเทอรัลในรูปแบบ T extends `$ {infer F}$ {K}$ {infer L}` ? `$ {F}$ {R[K]}$ {L}` : never ดังนั้นถ้า K ปรากฏอยู่ภายใน T มันจะถูกแทนที่ด้วย R[K] มิฉะนั้นเราจะได้ never.

เหตุผลที่วิธีนี้ใช้ได้และ K extends `$ {infer A}$ {infer RKey}$ {infer B}` ใช้ไม่ได้ เพราะ A, RKey และ B เป็นพารามิเตอร์ประเภททั่วไป ตัวแทน ทั้งหมด และตามคำอธิบายใน microsoft/TypeScript#40336 การ PR ที่ใช้ template literal types ระบุว่า “ตัวแทนที่ตามด้วยตัวแทนอีกตัวหนึ่งทันที จะถูกจับคู่โดยการสรุปตัวอักษรเดียวจากแหล่งที่มา” ซึ่งหมายความว่า A และ RKey จะยาวได้ไม่เกินหนึ่งตัวอักษรเท่านั้น ในขณะที่ใน `$ {infer F}$ {K}$ {infer L}`, K ไม่ใช่ตัวแทน

อย่างไรก็ตาม จากนั้นเราจะทำการเข้าดัชนีในประเภทที่แมปด้วย string & keyof R ซึ่งส่งผลให้เกิดสหสัมพันธ์ของการแทนที่สำหรับแต่ละคีย์ K ใน R เราคาดว่าการแทนที่จะมีเพียงตัวเดียวเท่านั้น ดังนั้นสหสัมพันธ์จึงอาจมีสมาชิกเพียงหนึ่งตัว หรืออาจเป็น never

ยูทิลิตี้ไทป์ OrDefault<T, U> ดูแล never หากมีการแทนที่ เราก็จะส่งคืนมัน มิฉะนั้นเราจะส่งคืนคีย์เดิม K ด้วยวิธีนี้ การแทนที่ที่ล้มเหลวจะไม่ลบคีย์ออกจากผลลัพธ์


ตกลง ลองทดสอบมันกันเถอะ:

type Replacements = {
    abc: 'cba';
    bcd: 'dcb';
};

type Test = {
    bcd: string;
    aabca: string;
};

type ReplacedInferred = ReplaceKeysInferred<Replacements, Test>;
/* type ReplacedInferred = {
    dcb: string;
    acbaa: string;
} */

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

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