网站搜索

typescript - 如何强制泛型参数只能是联合类型中的一种


我有以下代码(简化版):

const todoApi = {
    add(name: string) {},
    edit(id: number, name: string) {},
}

type TodoApi = typeof todoApi;
type TodoCommandName = keyof TodoApi

function createOptimisticUpdate<TCommand extends TodoApi[TodoCommandName]>(
    command: TCommand,
    localUpdate: (...p: Parameters<TCommand>) => void
) {
    return (...p: Parameters<TCommand>) => {
        localUpdate(...p);
        command(...p)   // should work but causes Type Error: A spread argument must either have a tuple type or be passed to a rest parameter.(2556)
        command(1)  // not expected to work but the error reveals expected params: (parameter) command: (arg0: never, name: string) => void, Expected 2 arguments, but got 1.(2554)
        command(...p as [never, string]) // workaround :(
    };
}

// from outside, when used with one command, everything works as expected
createOptimisticUpdate(todoApi.add, name => {})("name")
createOptimisticUpdate(todoApi.edit, (id, newName) => {})(1, "newName")
createOptimisticUpdate(todoApi.edit, (name) => {/* we have declared only one parameter with wrong name, but type is correctly inferred as number */})("newName")    // error: Expected 2 arguments, but got 1.

// but when we try to use more commands at once (not wanted), things are getting strange
let addOrEdit = Math.random() ? todoApi.add : todoApi.edit;
createOptimisticUpdate(addOrEdit, () =>{})("a")  // not expected: both signatures are valid
/*
type: function createOptimisticUpdate<((name: string) => void) | ((id: number, name: string) => void)>(command: ((name: string) => void) | ((id: number, name: string) => void), localUpdate: (...p: [name: string] | [id: number, name: string]) => void): (...p: [name: string] | [id: number, name: string]) => void
 */

TS 演示链接

当与一个命令一起使用时,一切按预期工作,但问题似乎是使用范围不限于此。有什么办法可以强制执行吗?

答案#1

如果约束是联合类型,就没有语法方法可以禁止联合,就像你遇到的这种情况一样。

我们确实有一些变通方法。一种方法是将 TCommand 与它自身的交集进行交集。如果 TCommand 是一个联合类型,则交集将产生不兼容,而如果 TCommand 是一个简单类型(或一个兼容的联合类型),则不会产生任何不兼容:

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
function createOptimisticUpdate<TCommand extends TodoApi[TodoCommandName]>(
    command: TCommand & UnionToIntersection<TCommand>,
    localUpdate: (...p: Parameters<TCommand>) => void
) {
    return (...p: Parameters<TCommand>) => {
        localUpdate(...p);
        (command as any)(...p)
    };
}
let addOrEdit = Math.random() ? todoApi.add : todoApi.edit;
createOptimisticUpdate(addOrEdit, () =>{})("a") // error 

游乐场链接

错误不是很严重,如果你使用字符串字面量类型来创建不兼容性,你可以更改它。错误稍微好一些,至少它指出了问题所在:

type ErrorType<TExpected, TActual, TError> = TExpected extends TActual ? unknown:  TError;
function createOptimisticUpdate<TCommand extends TodoApi[TodoCommandName]>(
    command: TCommand & ErrorType<TCommand, UnionToIntersection<TCommand>, "Unions are not supported">
    localUpdate: (...p: Parameters<TCommand>) => void
) {
   //....
}
let addOrEdit = Math.random() ? todoApi.add : todoApi.edit;
createOptimisticUpdate(addOrEdit, () =>{})("a") // error 

游乐场链接

另一种选择是使用一个非分配式的 Parameters 版本(预定义的版本是分配式的)。如果传入联合类型,这将防止创建可调用函数:

type ParametersNonDistributive<T> = [T] extends [(...a: infer P) => any]? P: never;
function createOptimisticUpdate<TCommand extends TodoApi[TodoCommandName]>(
    command: TCommand,
    localUpdate: (...p: ParametersNonDistributive<TCommand>) => void
) {
    return (...p: ParametersNonDistributive<TCommand>) => {
        localUpdate(...p);
        (command as any)(...p)
    };
}
let addOrEdit = Math.random() ? todoApi.add : todoApi.edit;
createOptimisticUpdate(addOrEdit, () =>{})("a") // error 
createOptimisticUpdate(todoApi.add, () =>{})("a") // ok

游乐场链接