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

使用扩展类型调用函数时丢失TypeScript类型

类型丢失问题排查

我在以下代码中某个环节丢失了类型信息:传入实际值调用函数时一切正常,但使用泛型继承类型时却报错。错误提示为:类型'StorageImageFormats[F]'上不存在属性'formatLocal'。尝试过用as强制指定返回类型,没有效果。

已尝试的解决方法

  • 移除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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 18:05:46