使用扩展类型调用函数时丢失TypeScript类型
类型丢失问题排查
我在以下代码中某个环节丢失了类型信息:传入实际值调用函数时一切正常,但使用泛型继承类型时却报错。错误提示为:类型'StorageImageFormatsas强制指定返回类型,没有效果。
已尝试的解决方法
- 移除
as const satisfies ...,直接声明const storageCategories: Record<string, StorageImageFormat>,虽然能解决错误,但丢失过多类型安全性——我需要TypeScript在访问storageCategories.chapter.imageFormats.entity_thumb这类不存在的属性时抛出错误。
问题代码
export interface StorageImageFormat { slug: string, formatLocal?: string, size: number, ratio: number | null, ext: "jpg" | "png", tint: null | { r: number, g: number, b: number }, } export interface StorageVideoFormat { slug: string, } export interface StorageCategory { folder: string; imageFormats: Record<string, StorageImageFormat>; videoFormats: Record<string, StorageVideoFormat>; } export const storageCategories = { chapter: { folder: "chapters", imageFormats: { original: { slug: "original", formatLocal: "original", size: 1000, ratio: null, ext: "jpg", tint: null, }, chapter_square: { slug: "chapter_square", formatLocal: "square", size: 250, ratio: 1, ext: "jpg", tint: null, }, chapter_bg: { slug: "chapter_bg", size: 1000, formatLocal: "bg_brown", ratio: null, ext: "jpg", tint: { r: 196, g: 189, b: 178 }, }, }, videoFormats: {}, }, entity: { folder: "entities", imageFormats: { original: { slug: "original", size: 1000, ratio: null, ext: "jpg", tint: null, }, entity_closeup: { slug: "entity_closeup", size: 100, ratio: 1, ext: "jpg", tint: null, }, entity_square: { slug: "entity_square", size: 150, ratio: 1, ext: "jpg", tint: null, }, entity_thumb: { slug: "entity_thumb", size: 150, ratio: null, ext: "jpg", tint: null, }, }, videoFormats: {}, } } as const satisfies Record<string, StorageCategory>; export type StorageCategorySlug = keyof typeof storageCategories; // IMAGE export type StorageImageFormats<C extends StorageCategorySlug> = typeof storageCategories[C]["imageFormats"]; export type StorageImageFormatSlug<C extends StorageCategorySlug> = keyof StorageImageFormats<C>; export type StorageImageFormatT<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>> = StorageImageFormats<C>[F]; export function getCategoryImageFormats<C extends StorageCategorySlug>( categorySlug: C, ) { return storageCategories[categorySlug].imageFormats as StorageImageFormats<C>; } export function getCategoryImageFormat<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>>( categorySlug: C, formatSlug: F ) { const categoryFormats = getCategoryImageFormats(categorySlug); return categoryFormats[formatSlug]; } export function getLocalUrl<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>>( categorySlug: C, formatSlug: F ) { const imageFormat = getCategoryImageFormat(categorySlug, formatSlug); // 错误:类型'StorageImageFormats<C>[F]'上不存在属性'formatLocal'。 if (!imageFormat.formatLocal) return null; return `assets/images/${imageFormat.formatLocal}_play.jpg`; } const imageFormat = getCategoryImageFormat("chapter", "original"); console.log(imageFormat.formatLocal); // 此处调用正常
问题原因与解决方案
原因
问题出在StorageImageFormats<C>的类型推导上:虽然storageCategories用了as const保留字面量类型,但StorageCategory接口里的imageFormats被声明为Record<string, StorageImageFormat>,这会让TypeScript在处理泛型时,无法跟踪具体某个imageFormat是否包含formatLocal属性——因为泛型C和F是任意符合约束的类型,TypeScript无法确定所有可能的StorageImageFormats<C>[F]都有formatLocal,哪怕实际传入的具体值有这个属性。
解决方案
我们需要让TypeScript保留每个具体imageFormat的字面量类型,而不是被StorageImageFormat接口“抹平”。可以通过调整类型定义,从storageCategories的实际值中提取类型,同时用satisfies约束结构:
修改后的代码如下:
export interface StorageImageFormat { slug: string, formatLocal?: string, size: number, ratio: number | null, ext: "jpg" | "png", tint: null | { r: number, g: number, b: number }, } export interface StorageVideoFormat { slug: string, } // 仅用于约束结构,不覆盖自动推导的字面量类型 interface StorageCategoryShape { folder: string; imageFormats: Record<string, StorageImageFormat>; videoFormats: Record<string, StorageVideoFormat>; } export const storageCategories = { chapter: { folder: "chapters", imageFormats: { original: { slug: "original", formatLocal: "original", size: 1000, ratio: null, ext: "jpg", tint: null, }, chapter_square: { slug: "chapter_square", formatLocal: "square", size: 250, ratio: 1, ext: "jpg", tint: null, }, chapter_bg: { slug: "chapter_bg", size: 1000, formatLocal: "bg_brown", ratio: null, ext: "jpg", tint: { r: 196, g: 189, b: 178 }, }, }, videoFormats: {}, }, entity: { folder: "entities", imageFormats: { original: { slug: "original", size: 1000, ratio: null, ext: "jpg", tint: null, }, entity_closeup: { slug: "entity_closeup", size: 100, ratio: 1, ext: "jpg", tint: null, }, entity_square: { slug: "entity_square", size: 150, ratio: 1, ext: "jpg", tint: null, }, entity_thumb: { slug: "entity_thumb", size: 150, ratio: null, ext: "jpg", tint: null, }, }, videoFormats: {}, } } as const satisfies Record<string, StorageCategoryShape>; export type StorageCategorySlug = keyof typeof storageCategories; // IMAGE export type StorageImageFormats<C extends StorageCategorySlug> = typeof storageCategories[C]["imageFormats"]; export type StorageImageFormatSlug<C extends StorageCategorySlug> = keyof StorageImageFormats<C>; export type StorageImageFormatT<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>> = StorageImageFormats<C>[F]; export function getCategoryImageFormats<C extends StorageCategorySlug>(categorySlug: C) { return storageCategories[categorySlug].imageFormats; } export function getCategoryImageFormat<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>>( categorySlug: C, formatSlug: F ) { const categoryFormats = getCategoryImageFormats(categorySlug); return categoryFormats[formatSlug]; } export function getLocalUrl<C extends StorageCategorySlug, F extends StorageImageFormatSlug<C>>( categorySlug: C, formatSlug: F ) { const imageFormat = getCategoryImageFormat(categorySlug, formatSlug); // TypeScript现在能识别formatLocal的存在性,不再报错 if (!imageFormat.formatLocal) return null; return `assets/images/${imageFormat.formatLocal}_play.jpg`; } const imageFormat = getCategoryImageFormat("chapter", "original"); console.log(imageFormat.formatLocal); // 正常工作
关键改动说明
- 把原来的
StorageCategory改名为StorageCategoryShape,仅用于satisfies约束,确保storageCategories结构符合预期,但不会覆盖自动推导的字面量类型; - 移除函数返回值里的
as StorageImageFormats<C>,TypeScript现在能自动推导更精确的类型; - 现在
StorageImageFormats<C>[F]会保留每个具体格式的完整类型,包括formatLocal的存在性,TypeScript能正确识别该属性可能为undefined或具体字符串,因此不会报错。
内容的提问来源于stack exchange,提问作者Maslow
相关产品推荐
相关产品推荐

