You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript中req.file.key/location类型错误求助(Express+Multer-S3)

解决Express+Multer-S3上传AWS S3时的TypeScript类型错误

问题描述

使用Express框架结合Multer-S3中间件将图片上传至AWS S3时,遇到TypeScript类型错误:

  • Property 'key' does not exist on type 'File'
  • Property 'location' does not exist on type 'File'

错误出现在product.ts文件中访问req.file.key和req.file.location的位置。已尝试在项目根目录创建types文件夹并编写声明文件,且将其加入tsconfig.json的include列表,但问题仍未解决。


相关代码

product.ts关键代码

/*modules*/
import express from 'express';
/*models*/
import Raffle, { IRaffle } from '../../models/Raffle';
import RafflePlayer from '../../models/RafflePlayer';
import MystBox, { IMystBox } from '../../models/MystBox';
import MystBoxPlayer from '../../models/MystBoxPlayer';
import BreakCode from '../../models/BreakCode';
import WlOrMint from '../../models/WlOrMint';
import SimpleCard from '../../models/SimpleCard';
/*middleware*/
import fetchNFTData from '../../middleware/fetchNftData';
import connectAwsS3 from '../../middleware/connect-awsS3';
/*fetch*/
import getMetadatNft from '../../services/metadataNft';
import BreakEncrypt from '../../models/BreakEncrypt';
import BreakGuess from '../../models/BreakGuess';
import BreakPlayer from '../../models/BreakPlayer';

router.post(
    '/addmyst',
    connectAwsS3.uploadCardImage.single('imagemystbox'),
    async (req, res) => {
        if (!req.file) {
            return res.status(400).json({
                error:
                    'No image provided or your image has not the extenions jpeg|jpg|png. Try again!',
            });
        }
        try {
            const {
                title,
                nbrmystbox,
                exp,
                price,
                posted,
                startdate,
                enddate,
                currentdate,
            } = req.body;

            console.log('reqbody', req.body);

            console.log(
                'var',
                title,
                nbrmystbox,
                exp,
                price,
                startdate,
                enddate,
                currentdate
            );

            let error = '';
            const prizes = JSON.parse(req.body.prizes);
            console.log('prizes', prizes, typeof prizes);

            if (!title.trim()) {
                error = 'Title is empty.';
            } else if (!nbrmystbox) {
                error = 'Nbr mystBox is empty.';
            } else if (!price) {
                error = 'Price is empty.';
            } else if (!exp) {
                error = 'Exp is empty.';
            } else if (!startdate) {
                error = 'Start date is empty.';
            } else if (!enddate) {
                error = 'End date is empty.';
            } else if (prizes.length === 0) {
                error = 'No prize added.';
            }

            if (error) {
                // Suppression de l'image de S3 en cas d'erreur
                if (req.file && req.file.key) {
                    const cleanedKey = req.file.key.replace('MystBox/', ''); // Supprime le préfixe

                    console.log('deletefile1', req.file, req.file.key);
                    await connectAwsS3.deleteImageFromS3(cleanedKey, 'MystBox');
                }

                return res.status(400).json({ error });
            }

            const imageUrl = req.file.location;
            console.log('imageUrl :>> ', imageUrl);

            const mystBoxData: IMystBox = new MystBox({
                title,
                nbrmystbox,
                prize: prizes,
                exp,
                price,
                posted,
                image: imageUrl,
            });

            console.log('mystboxdata1', mystBoxData);
            if (posted === 'on') {
                const currentDate = new Date(currentdate);
                const startDate = new Date(startdate);
                const endDate = new Date(enddate);

                if (currentDate < startDate) {
                    mystBoxData.status = 'Upcoming';
                } else if (currentDate >= startDate && currentDate <= endDate) {
                    mystBoxData.status = 'Ongoing';
                } else {
                    mystBoxData.status = 'Ended';
                }

                mystBoxData.startdate = startDate;
                mystBoxData.enddate = endDate;
            }
            console.log('mystboxdata2', mystBoxData);

            await mystBoxData.save();

            return res.status(201).json({ message: 'Mysterybox added successfully.' });
        } catch (err: any) {
            let errorMessage = 'Impossible to create the mysterybox!';

            // Gestion des erreurs
            if (err.code === 11000) {
                errorMessage = 'That title already exists!';
            }

            // Suppression de l'image de S3 si elle a été téléchargée
            if (req.file && req.file.key) {
                const cleanedKey = req.file.key.replace('MystBox/', ''); // Supprime le préfixe

                console.log('deletefile2', req.file, req.file.key);
                await connectAwsS3.deleteImageFromS3(cleanedKey, 'MystBox');
            }

            return res.status(400).json({ error: errorMessage });
        }
    }
);

connect-awsS3.ts中间件代码

/* eslint-disable no-console -- console*/

import path from 'node:path';
import multer from 'multer';
import multerS3 from 'multer-s3';
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
import { Request, Response, NextFunction } from 'express'; // Ajout des types

const s3Client = new S3Client({
    region: process.env.AWS_REGION as string,
    credentials: {
        accessKeyId: process.env.ACCESS_KEY_S3 as string,
        secretAccessKey: process.env.SECRET_KEY_S3 as string,
    },
});
const bucketName = process.env.AWS_BUCKET_NAME!;

// Middleware pour l'upload
const upload = multer({
    storage: multerS3({
        s3: s3Client,
        bucket: bucketName,
        async key(req, file, cb) {
            let folder = ''; // Initialisation du dossier

            if (file && file.fieldname === 'imagemystbox') {
                folder = 'MystBox/';
            } else if (file && file.fieldname === 'imagebreakcode') {
                folder = 'BreakCode/';
            } else if (file && file.fieldname === 'questFiles') {
                folder = 'Quest/';
            } else if (file && file.fieldname === 'imagewlormint') {
                folder = 'WlOrMint/';
            } else if (file && file.fieldname === 'airdropFiles') {
                folder = 'Airdrop/';
            }

            // Ajouter d'autres conditions pour d'autres types si nécessaire
            console.log('Generated folder:', folder); // Vérifie le dossier généré

            const fileExtension = path.extname(file.originalname).toLowerCase();
            const newFilename = uuidv4() + fileExtension; // Génère un nouvel identifiant unique pour le nom du fichier

            cb(null, folder + newFilename); // Utilisez le nouveau nom de fichier
        },
        acl: 'public-read',
        metadata(req, file, cb) {
            cb(null, { fieldName: file.fieldname });
        },
    }),
    limits: { fileSize: 10 * 1024 * 1024 }, // Max file size is 10MB
    fileFilter(req: Request, file, cb) {
        const fileTypes = /jpeg|jpg|png/;
        const extname = fileTypes.test(path.extname(file.originalname).toLowerCase());
        const mimetype = fileTypes.test(file.mimetype);
        console.log('Nom du fichier :', file.originalname); // Ajout du console.log ici

        if (extname && mimetype) {
            return cb(null, true);
        }
        // Signaler une erreur de type de fichier à la route
        req.invalidFileType = true;
        cb(null, false);
    },
});

// Fonction pour supprimer l'image de S3
const deleteImageFromS3 = async (key: string, type: string) => {
    const folder = `${type}/`; // Dossier basé sur le type

    const params = {
        Bucket: bucketName,
        Key: folder + key,
    };

    try {
        await s3Client.send(new DeleteObjectCommand(params));
        console.log(`Successfully deleted ${key} from S3`);
    } catch (err) {
        console.error(`Error deleting ${key} from S3:`, err);
    }
};

export default {
    uploadCardImage: upload,
    deleteImageFromS3,
};

原自定义声明文件(无效)

declare module 'connect-awsS3' {
    import { Request, Response, NextFunction } from 'express';

    interface File {
        key: string;
        location: string;
    }

    type Upload = (req: Request, res: Response, next: NextFunction) => void;

    type DeleteImageFromS3 = (key: string, type: string) => Promise<void>;

    const uploadCardImage: Upload;
    const deleteImageFromS3: DeleteImageFromS3;

    export { uploadCardImage, deleteImageFromS3 };
}

tsconfig.json配置

{
    "extends": "@repo/typescript-config/base.json",
    "compilerOptions": {
        "lib": ["ES2015"],
        "outDir": "./dist",
        "rootDir": "./src",
        "esModuleInterop": true
        // Autres options de configuration spécifiques à votre projet...
    },
    "exclude": ["node_modules"],
    "include": [
        "src",
        "types" // Assurez-vous que "types" est bien inclus s'il s'agit du répertoire où se trouve connect-awsS3.d.ts
    ]
}

解决方案

1. 修正自定义声明文件

原声明文件错误地声明了模块,正确做法是扩展Express的Request接口和Multer的File类型,添加key和location属性。在types文件夹下创建express.d.ts文件:

// types/express.d.ts
declare namespace Express {
  interface Request {
    // 扩展req.file类型,添加multer-s3返回的key和location
    file?: import('multer').File & {
      key: string;
      location: string;
    };
    // 保留自定义的invalidFileType属性
    invalidFileType?: boolean;
  }
}

// 全局扩展multer的File类型,让所有使用multer的地方都能识别key和location
declare module 'multer' {
  interface File {
    key?: string;
    location?: string;
  }
}

2. 确保tsconfig.json正确包含类型文件

修改tsconfig.json的include字段,确保所有类型文件被正确识别:

"include": [
  "src/**/*",
  "types/**/*.d.ts"
]

3. 重启TypeScript服务

如果使用VSCode,按Ctrl+Shift+P(Windows)或Cmd+Shift+P(Mac)执行TypeScript: Restart TS Server,确保新的类型声明被加载。


内容的提问来源于stack exchange,提问作者degen balls

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 04:54:53