移除可调用签名的只读修饰符

9

在ES6中,函数最初不允许更改只读属性(例如name):

let foo = function (n: number) {
    return n;
}

foo.name = 'not foo'; // Cannot assign to 'name' because it is a read-only property

为了解决这个问题,使用了参考文献中的Writable实用程序类型。
type Writable<T> = {
  -readonly [K in keyof T]: T[K];
};

1. 只读属性name不受交集影响:

let writableFoo: typeof foo & { name: string } = foo;
writableFoo.name = 'not foo'; // Cannot assign to 'name' because it is a read-only property

2. Writable 无法从函数类型获取 name,也无法调用:

let writableFoo: Writable<typeof foo> = foo;
writableFoo.name = 'not foo'; // Property 'name' does not exist on type 'Writable<(n: number) => number>'
writableFoo(1); // This expression is not callable

3. WritableFunction 中获取 name,但仍然无法调用:

let writableFoo: Writable<Function> = foo;
writableFoo.name = 'not foo';
writableFoo(1); // This expression is not callable

4. Omit 使用索引签名,并且也不可调用:

let writableFoo: Omit<typeof foo, 'name'> & { name: string } = foo;
writableFoo.name = 'not foo';
writableFoo(1); // This expression is not callable

这里的目标是输入 writableFoo 以保持其可调用性并允许更改name,最好不要使用其他具有Writable的属性。它不试图解决特定编码问题,而是研究指定类型问题。

为什么1不会影响交集类型的readonly修饰符?

为什么2没有获取name,尽管它在typeof foo中被识别为foo.name

如何使2-4可以获得调用签名并从name中移除readonly修饰符?


如果我查看MDN,它指定如果你想要更改它,你需要使用Object.defineProperty,因为它是可配置的,你可以通过这种方式覆盖它。 - Icepickle
@Icepickle感谢您,这是正确的(但对于可写非可配置属性不起作用)。正如我所指出的,目标不是解决编码问题,而是调查TS类型问题。 - Estus Flask
为什么1不会影响只读修饰符的联合类型? typeof foo & { name: string } 创建的是一个交集,而不是一个联合类型 typeof foo | { name: string } - Naresh Pingale
@NareshPingale 谢谢您的注意,已经修正术语。我预期它是一个交集,联合操作在这里行不通。 - Estus Flask
@EstusFlask,下面的新答案有帮助吗?如果有,请接受。 - TSR
显示剩余2条评论
1个回答

2

使用这个功能,您可以同时调用和写入name

interface Foo extends Function{
    name: string
}
const d: Foo = function(){}
d.name ='not foo'
d()

感谢您。从技术上讲,它解决了问题,但是该函数忽略了函数签名,d('should cause TS error', 'for unexpected args')(这在问题中没有提到,但似乎是合理要求)。同时,const本身也能够消除错误,因为它与let的处理方式不同。
type  Incrementer = (x: number)=>string
interface Foo extends Incrementer{
    name: string
}
let d: Foo = (x)=>'a'+x
d.name ='not foo'
d(1)
d('s') // string not expceted must be number

如果你想要一个通用的答案,这里有你所需要的 可写
interface Writable<T extends (...args: any) => any> {
    (...arg: Parameters<T>): ReturnType<T>;

    name: string
}

type  Incrementer = (x: number, y: boolean) => string

let d: Writable<Incrementer> = (x, y) => 'a' + x + y
d.name = 'not foo' // no error
d(1, true) // no error
d('d', true)  // 'd' is not assignable to number
d(2, 1) // 1 is not assignable to boolean

谢谢。它在技术上解决了问题,但是Function忽略了函数的签名,d('should cause TS error', 'for unexpected args')(这在问题中没有提到,但似乎是一个合理的要求)。此外,const本身就能够消除错误,因为它与let的处理方式不同。 - Estus Flask
2
好的答案,已点赞。只想补充一下,你可以在接口声明中直接编写可调用签名,例如:interface Foo { (n: number): number; name: string },无需使用 extends - hackape
@EstusFlask,请检查最后一次编辑。我用一个通用答案改进了回答。 - TSR
谢谢,这很有帮助。我会保持问题开放,因为我仍然对TS内部的可调用签名行为(问题中的“为什么”部分)感兴趣。如果有一种方法可以在不显式指定函数类型的情况下推断出签名,则这将是首选解决方案。 - Estus Flask

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接