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

typescript - นำเข้าคลาสในไฟล์นิยาม (*d.ts)


ฉันต้องการขยายการกำหนดประเภทของ Express Session เพื่ออนุญาตให้ใช้ข้อมูลที่กำหนดเองของฉันในที่เก็บเซสชัน ฉันมีอ็อบเจ็กต์ req.session.user ซึ่งเป็นอินสแตนซ์ของคลาสของฉัน User:

export class User {
    public login: string;
    public hashedPassword: string;

    constructor(login?: string, password?: string) {
        this.login = login || "" ;
        this.hashedPassword = password ? UserHelper.hashPassword(password) : "";
    }
}

ดังนั้นฉันจึงสร้างไฟล์ own.d.ts ของตัวเองเพื่อรวมคำนิยามกับไทป์ของเซสชัน Express ที่มีอยู่:

import { User } from "./models/user";

declare module Express {
    export interface Session {
        user: User;
    }
}

แต่มันไม่ทำงานเลย - VS Code และ tsc มองไม่เห็นมัน ดังนั้นฉันจึงสร้างนิยามทดสอบด้วยประเภทง่าย ๆ:

declare module Express {
    export interface Session {
        test: string;
    }
}

และสนามทดสอบทำงานได้ดี ดังนั้นการนำเข้าจึงเป็นสาเหตุของปัญหา

ฉันก็ลองเพิ่ม /// <reference path='models/user.ts'/> แทนการใช้ import แต่ tsc ก็ไม่เห็นคลาส User - แล้วฉันจะใช้คลาสของตัวเองในไฟล์ *d.ts ได้อย่างไร?

แก้ไข: ฉันตั้งค่า tsc ให้สร้างไฟล์คำจำกัดความเมื่อคอมไพล์และตอนนี้ฉันมี user.d.ts ของฉันแล้ว:

export declare class User {
    login: string;
    hashedPassword: string;
    constructor();
    constructor(login: string, password: string);
}

และไฟล์การพิมพ์ของตัวเองสำหรับการขยาย Express Session:

import { User } from "./models/user";
declare module Express {
    export interface Session {
        user: User;
        uuid: string;
    }
}

แต่ยังไม่ทำงานเมื่อมีคำสั่ง import อยู่ข้างบน มีไอเดียอะไรไหม?

คำตอบ #1

หลังจากพัฒนาด้วย TypeScript มาสองปี ในที่สุดฉันก็สามารถแก้ปัญหานี้ได้

โดยพื้นฐานแล้ว TypeScript มีการประกาศโมดูลอยู่สองประเภท: "local" (โมดูลปกติ) และ ambient (ทั่วโลก) ประเภทที่สองอนุญาตให้เขียนการประกาศโมดูลทั่วโลกที่ถูกรวมกับการประกาศโมดูลที่มีอยู่แล้ว ความแตกต่างระหว่างไฟล์เหล่านี้คืออะไร?

ไฟล์ d.ts จะถูกพิจารณาเป็นการประกาศโมดูลแบบแอมเบียนต์เฉพาะเมื่อพวกมันไม่มีการนำเข้าใด ๆ หากคุณมีบรรทัดนำเข้า ไฟล์นั้นจะถูกพิจารณาเป็นไฟล์โมดูลปกติ ไม่ใช่ไฟล์โกลบอล ดังนั้นการขยายการกำหนดโมดูลจะไม่ทำงาน

ดังนั้นนี่จึงเป็นเหตุผลว่าทำไมวิธีแก้ปัญหาทั้งหมดที่เราพูดถึงที่นี่ถึงไม่ทำงาน แต่โชคดีที่ตั้งแต่ TS 2.9 เป็นต้นมา เราสามารถนำเข้าประเภทเข้าไปในการประกาศโมดูลระดับโลกโดยใช้ไวยากรณ์ import():

declare namespace Express {
  interface Request {
    user: import("./user").User;
  }
}

ดังนั้นบรรทัด import("./user").User; จึงทำให้เกิดเวทมนตร์และตอนนี้ทุกอย่างทำงานได้ :)

คำตอบ #2

ขอบคุณสำหรับคำตอบจากMichał Lytek นี่เป็นอีกวิธีหนึ่งที่ฉันใช้ในโครงการของฉัน

เราสามารถนำเข้า User และ ใช้งานซ้ำ หลายครั้งโดยไม่ต้องเขียน import("./user").User ทุกที่ และยังสามารถ นำไปใช้ หรือ ส่งออกซ้ำ ได้อีกด้วย

declare namespace Express {
    type User = import('./user').User;

    export interface Request {
        user: User;
        target: User;
        friend: User;
    }

    export class SuperUser implements User {
        superPower: string;
    }

    export { User as ExpressUser }
}

สนุกนะ :)

คำตอบ #3

เพื่อความสมบูรณ์:

  • ถ้าคุณมีการประกาศโมดูลแบบแอมเบียนท์ (เช่น ไม่มีการนำเข้า/ส่งออกระดับบนสุด) มันจะสามารถใช้ได้ทั่วโลกโดยไม่จำเป็นต้องนำเข้ามาโดยตรงที่ใด แต่ถ้าคุณมีการประกาศโมดูล คุณจะต้องนำเข้ามาในไฟล์ที่ใช้งานด้วย
  • หากคุณต้องการนำเข้าประเภทที่มีอยู่แล้ว (ซึ่งถูกส่งออกจากไฟล์อื่น) ในการประกาศโมดูลแบบแอมเบียนท์ของคุณ คุณไม่สามารถทำได้ด้วยการนำเข้าระดับสูงสุด (เพราะถ้าเป็นเช่นนั้นมันจะไม่เหลือการประกาศแบบแอมเบียนท์)

ดังนั้นถ้าคุณทำสิ่งนี้: (https://stackoverflow.com/a/39132319/2054671)

// index.d.ts
import { User } from "./models/user";
declare module 'express' {
  interface Session {
    user: User;
    uuid: string;
  }
}

นี่จะเพิ่มโมดูล 'express' ที่มีอยู่ด้วยอินเทอร์เฟซใหม่นี้ https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation

แต่การจะใช้สิ่งนี้คุณจะต้องนำเข้าสิ่งนี้ในไฟล์คอนซูเมอร์ของคุณ มันจะไม่สามารถใช้งานได้โดยค่าเริ่มต้นทั่วโลกเหมือนการประกาศแบบแอมเบียนต์เพราะว่ามันไม่ใช่การประกาศแบบแอมเบียนต์อีกต่อไป

  • ดังนั้น ในการนำเข้าประเภทที่มีอยู่ซึ่งส่งออกมาจากไฟล์อื่น คุณต้องนำเข้ามันภายในบล็อก declare (พูดถึงตัวอย่างนี้ ในตัวอย่างอื่นที่คุณไม่ได้ประกาศโมดูล คุณสามารถนำเข้าแบบอินไลน์ในที่อื่นได้)

  • ในการทำสิ่งนี้ คุณไม่สามารถใช้การนำเข้าปกติแบบนี้ได้

    declare module B {
      import A from '../A'
      const a: A;
    }
    

    เนื่องจากในการใช้งานปัจจุบัน กฎเกณฑ์สำหรับการแก้ไขโมดูลที่ถูกนำเข้านี้ค่อนข้างสับสน และดังนั้น ts จึงไม่อนุญาต สิ่งนี้คือเหตุผลของข้อผิดพลาด Import or export declaration in an ambient module declaration cannot reference module through relative module name. (ผมไม่สามารถหาลิงก์ไปยังปัญหาใน github ที่เกี่ยวข้องได้ หากใครพบ กรุณาแก้ไขคำตอบนี้และกล่าวถึง https://github.com/microsoft/TypeScript/issues/1720)

    โปรดทราบ คุณยังสามารถทำอะไรแบบนี้ได้:

    declare module B {
      import React from 'react';
      const a: A;
    }
    

    เพราะนี่เป็นการนำเข้าพาธแบบสมบูรณ์และไม่ใช่การนำเข้าพาธแบบสัมพัทธ์

  • ดังนั้นวิธีเดียวที่จะทำสิ่งนี้อย่างถูกต้องในโมดูลแอมเบียนท์คือการใช้ไวยากรณ์การนำเข้าแบบไดนามิก (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#import-types)

    declare namespace Express {
     interface Request {
       user: import("./user").User;
     }
    }
    

    ตามที่ระบุไว้ในคำตอบที่ได้รับการยอมรับ (https://stackoverflow.com/a/51114250/2054671)

  • คุณยังสามารถทำการเพิ่มข้อมูลทั่วโลกด้วยบางสิ่งแบบนี้ได้ด้วย:

    import express = require('express');
    import { User } from "../models/user";
    
    declare global {
        namespace Express {
            interface Session {
                user: User;
                uuid: string;
            }
        }
    }
    

    แต่จำไว้ว่าการขยายแบบทั่วโลกสามารถทำได้เฉพาะในโมดูล ไม่ใช่ในประกาศแวดล้อม ดังนั้นสิ่งนี้จะทำงานได้ก็ต่อเมื่อคุณนำเข้ามันในไฟล์ของผู้บริโภค ตามที่กล่าวไว้ในคำตอบของ @masa (https://stackoverflow.com/a/55721549/2054671)


ทุกประเด็นที่กล่าวมาข้างต้นใช้ได้กับการนำเข้ามอดูลที่ถูกส่งออกมาจากที่อื่น ในมอดูลแอมเบียนต์ของคุณ แต่การนำเข้ามอดูลแอมเบียนต์ในมอดูลแอมเบียนต์อื่นละ? (นี่มีประโยชน์ถ้าคุณต้องการใช้การประกาศแอมเบียนต์ที่มีอยู่ในประกาศมอดูลแอมเบียนต์ของคุณเองและทำให้แน่ใจว่าชนิดแอมเบียนต์เหล่านั้นสามารถมองเห็นได้ในผู้บริโภคของมอดูลแอมเบียนต์ของคุณด้วย)

  • คุณสามารถใช้ไดเร็กทีฟ /// <reference types="../../a" />

    // ambientA.d.ts
    interface A {
      t: string
    }
    
    // ambientB.d.ts
    /// <reference types="../ambientA.d.ts" />
    declare module B {
      const a: A;
      export { a };
    }
    

ลิงก์ไปยังคำตอบที่เกี่ยวข้องอื่น ๆ:

  • การประกาศแบบ Ambient กับประเภทที่นำเข้ามาใน TypeScript

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