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

基于类型键值的TypeScript泛型函数参数类型校验问题

TypeScript泛型函数中已定义函数表达式的类型校验问题解决

问题背景

我需要实现一个TypeScript泛型函数:第一个参数是指定类型的键名,第二个回调函数的返回值必须匹配该键对应的类型(比如传入'title',回调只能返回string类型)。目前内联回调时类型校验正常,但传入已定义的函数表达式时无法正确校验,具体代码如下:

type Events = {
    title: string;
    cost: number;
}

type KeyType<T, K extends keyof T> = T[K]; // 查找类型键对应的类型

type CallBack<T extends keyof Events> = (...args: any[]) => KeyType<Events, T>;

const subscribers: Map<keyof Events, CallBack<keyof Events>[]> = new Map();

const subscribe = <T extends keyof Events>(eventName: T, callback: CallBack<T>) => {
    const lookup = subscribers.get(eventName)
    if (!lookup) {
        subscribers.set(eventName, [callback])
    } else {
        lookup.push(callback)
    }
}

subscribe('title', () => { return "hello" }) // ok

const returnString: CallBack<'title'> = () => { return 'one' };
const returnNumber: CallBack<'cost'> = () => { return 789789789 };
const returnNumberInfer = () => { return 1 };

subscribe('title', returnString) // ok
subscribe('cost', returnNumber) // ok

subscribe('title', returnNumber) // 本应报错但未报错
subscribe<'title'>('title', returnNumber) // 指定泛型后正常,但不直观
subscribe('title', returnNumberInfer) // 未标注类型时可通过校验

// 内联声明时校验正常
subscribe('cost', () => { return 789789789 }) // ok
subscribe('title', () => { return 789789789 }) // error

问题原因

出现这个问题的核心是TypeScript的联合类型协变特性和泛型推断逻辑:

  1. 原代码中CallBack<keyof Events>是联合类型函数:(() => string) | (() => number)。由于函数返回值类型是协变的,() => number属于这个联合类型的子类型。
  2. 当传入returnNumber(类型为CallBack<'cost'>)到subscribe('title', ...)时,TypeScript会自动推断泛型参数T为'title' | 'cost'(因为T extends keyof Events允许联合类型),此时CallBack<T>正好匹配returnNumber的类型,因此不会触发报错。
  3. 内联回调时,TypeScript会直接根据eventName的字面量类型推断T为单一键类型(比如'title'),所以能正确校验返回值类型。

解决方案

方案一:函数重载强制单一键匹配

为每个键单独定义函数重载,让TypeScript严格绑定eventName和回调的类型对应关系:

type Events = {
    title: string;
    cost: number;
}

type CallBack<K extends keyof Events> = (...args: any[]) => Events[K];

const subscribers: Map<keyof Events, CallBack<keyof Events>[]> = new Map();

// 为每个键定义重载规则
function subscribe(eventName: 'title', callback: CallBack<'title'>): void;
function subscribe(eventName: 'cost', callback: CallBack<'cost'>): void;

// 通用实现逻辑
function subscribe<K extends keyof Events>(eventName: K, callback: CallBack<K>): void {
    const lookup = subscribers.get(eventName);
    if (!lookup) {
        subscribers.set(eventName, [callback]);
    } else {
        lookup.push(callback);
    }
}

// 测试验证
subscribe('title', () => "hello") // ok

const returnString: CallBack<'title'> = () => 'one';
const returnNumber: CallBack<'cost'> = () => 789789789;
const returnNumberInfer = () => 1;

subscribe('title', returnString) // ok
subscribe('cost', returnNumber) // ok

subscribe('title', returnNumber) // 报错:类型不匹配
subscribe('title', returnNumberInfer) // 报错:类型不匹配

subscribe('cost', () => 789789789) // ok
subscribe('title', () => 789789789) // error

方案二:简化回调类型约束泛型推断

去掉单独的CallBack类型定义,直接在subscribe函数中约束回调返回值与eventName的对应关系,避免联合类型推断问题:

type Events = {
    title: string;
    cost: number;
}

const subscribers: Map<keyof Events, Array<(...args: any[]) => Events[keyof Events]>> = new Map();

const subscribe = <K extends keyof Events>(
    eventName: K,
    callback: (...args: any[]) => Events[K]
) => {
    const lookup = subscribers.get(eventName);
    if (!lookup) {
        subscribers.set(eventName, [callback]);
    } else {
        lookup.push(callback);
    }
}

// 测试验证
subscribe('title', () => "hello") // ok

const returnString = () => 'one';
const returnNumber = () => 789789789;
const returnNumberInfer = () => 1;

subscribe('title', returnString) // ok
subscribe('cost', returnNumber) // ok

subscribe('title', returnNumber) // 报错:类型不匹配
subscribe('title', returnNumberInfer) // 报错:类型不匹配

subscribe('cost', () => 789789789) // ok
subscribe('title', () => 789789789) // error

方案选择

  • 方案一适合键数量较少的场景,类型匹配规则更直观,可读性强。
  • 方案二更简洁,适合键数量较多的场景,无需为每个键单独定义重载。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 00:23:12