我正在开发一个模块,我想让用户定义一个"允许的类型"列表,以便在其他方法中使用,但我正在努力让它在TypeScrip中正常工作:

function initSomething<T extends ArrayLike<string>>(eventTypes:T) {
  type EventType = T[number];
  
  function doTheThing(type:EventType) {
    console.log(type, eventTypes);
  }
  
  return { doTheThing, eventTypes };
}

// I'd like this to throw an error: 'potato' is not one of ['hello', 'bye']
initSomething(['hello', 'bye']).doTheThing('potato');

我知道将字符串数组转换为const以将其转换为字符串literal union类型的技巧:

const eventTypes = ['hello', 'bye'] as const
type EventType = typeof eventTypes[number]

但我想不出如何使其适应我的情况,在我的情况下,数组是泛型

我设法做了一些事情,但这似乎很奇怪:我用keyof制作了一张&lt;EventType、Any&gt;的 map .

function initSomething<T>(eventTypes:(keyof T)[]) {
  type EventType = keyof T;

  function doTheThing(type:EventType) {
    console.log(type, eventTypes);
  }
  
  return { doTheThing, eventTypes };
}

// Getting the error below, as expected. Perfect!
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.
initSomething(['hello', 'bye']).doTheThing('potato');

有没有一种方法可以做同样的事情,而不用这个奇怪的keyof技巧,而是从数组中生成一个简单的字符串联合?

推荐答案

您的代码的问题在于,打字脚本通常会推断,像['hello', 'bye']这样的array literal具有类型string[].这通常是人们想要的类型,如下面的代码所示:

const arr = ['hello', 'bye'];
//    ^? const arr: string[]
arr.push('howdy'); // okay

如果每次初始化字符串数组时,编译器都假设它只能包含您放入其中的literal个值,按它们的确切顺序,这将是非常恼人的.但有时您do希望编译器假定这一点,例如在您的例子中(如果不是顺序,至少是字面值).


正如您所注意到的,您可以使用a const assertion(这里不鼓励使用术语"cast")来实现这种行为:

const arr = ['hello', 'bye'] as const;
// const arr: readonly ["hello", "bye"]
arr.push("howdy"); // error

事实上,你可以在你的initSomething()版本中使用它:

initSomething(['hello', 'bye'] as const).doTheThing('potato'); // error
// -----------------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.

但您可能不希望来电者需要记住这样做.


一种方法是在函数中使用const type parameter,如下所示:

function initSomething<const EventTypes extends ArrayLike<string>>(eventTypes: EventTypes) {
    //                 ^^^^^
    type EventType = EventTypes[number];

    function doTheThing(type: EventType) {
        console.log(type, eventTypes);
    }

    return { doTheThing, eventTypes };
}

这告诉编译器或多或少地推断EventTypes,就好像调用方指定了as const一样.您可以看到它的工作情况与预期一致:

initSomething(['hello', 'bye']).doTheThing('potato');
// --------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.

不过,至少在这个例子中,您可能不需要这种复杂性. 看起来你并不真正关心EventTypes类型的参数,只是你从它计算出的EventType.在这种情况下,你可以直接在EventType中使你的函数泛型化:

function initSomething<EventType extends string>(eventTypes: ArrayLike<EventType>) {
    function doTheThing(type: EventType) {
        console.log(type, eventTypes);
    }
    return { doTheThing, eventTypes };
}
initSomething(['hello', 'bye']).doTheThing('potato');
// --------------------------------------> ~~~~~~~~
// Argument of type '"potato"' is not assignable to parameter of type '"hello" | "bye"'.

这是可行的,因为当类型参数从constrainedstring时,编译器假定您关心它的字符串文字类型(或此类类型的union).也就是说,extends string将为编译器提供context,其中将保留文字类型.

Playground link to code

Typescript相关问答推荐

类型脚本不会推断键的值类型

如何将绑定到实例的类方法复制到类型脚本中的普通对象?

动态调用类型化函数

contextPaneTitleText类型的参数不能分配给remoteconfig类型的关键参数'""'''

更新为Angular 17后的可选链运算符&?&问题

如何在单击停止录制按钮后停用摄像机?(使用React.js和Reaction-Media-Recorder)

未在DIST中正确编译TypeScrip src模块导入

如何在另一个参数类型中获取一个参数字段的类型?

在包含泛型的类型脚本中引用递归类型A<;-&B

TS2339:类型{}上不存在属性消息

React Typescript项目问题有Redux-Toolkit userSlice角色问题

基于未定义属性的条件属性

如何使用泛型函数参数定义类型脚本函数

可以将JS文件放在tsconfig';s includes/files属性?或者,我如何让tsc判断TS项目中的JS文件?

T的typeof键的Typescript定义

是否使用类型将方法动态添加到类?

有没有一种简单的方法可以访问Keyboard Shortcuts JSON文件中列出的每个命令的显示名称(标题)?

可选通用映射器函数出错

递归类型名称

使用 Next.js 路由重定向到原始 URL 不起作用