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
