Typescript: 当接口继承时,接口属性名称的联合

huangapple go评论79阅读模式
英文:

Typescript: union of interface properties names when interface inherits

问题

以下是您提供的代码的翻译部分:

export interface SingleAction {
  //...
}

export interface Actions {
  [key: string]: SingleAction
}

//

export interface ExampleActions extends Actions {
  testAction: {
    //...
  }

  testAction2: {
    //...
  }
}

抽象类型 Actions 仅用于保护真实 ActionList 类型的形状(例如 ExampleActions

现在我们需要一个类型,它是 ExampleActions 中所有属性名称的联合:

// 期望 Test 为 "testAction" | "testAction2" 但实际上是字符串
type Test = keyof ExampleActions;

const a: Test = "gfhgfh"; // 有效,但应该仅需要 "testAction" | "testAction2"

当我在 export interface ExampleActions extends Actions 行中去掉 extends Actions 时,它按预期工作。

如何在不去掉它的情况下实现这一点?

为什么我需要它?

我想让某个类知道某个方法调用中允许的参数,就像这样:

class ActionsManager<CALLABLE_ACTIONS extends Actions> {
  callAction(name: keyof CALLABLE_ACTIONS) {
    //...
  }
}

const am = new ActionsManager<ExampleActions>();

am.callAction("testAction"); // 正确
am.callAction("testAction2"); // 正确
am.callAction("anotherAction"); // 错误,"ExampleActions" 接口中没有属性 "anotherAction"

可能相关的链接:

https://github.com/microsoft/TypeScript/issues/24274

https://github.com/DanielXMoore/Civet/issues/271

看起来现在可能不可能?但有关键字 satisfies,一些人考虑将其引入到 TypeScript 语法中。

英文:

We have that code:

export interface SingleAction {
//...
}

export interface Actions {
    [key: string]: SingleAction
}

//

export interface ExampleActions extends Actions {
    testAction: {
        //...
    }

    testAction2: {
        //...
    }
}

Abstract type Actions is just for guarding shapes of real ActionList like types (for example ExampleActions)

Now we need type that is union of all properties names in ExampleActions:

// expecting Test to be &quot;testAction&quot; | &quot;testAction2&quot; but instead it&#39;s string
type Test = keyof ExampleActions; 

const a: Test = &quot;gfhgfh&quot;; // valid, but should require only &quot;testAction&quot; | &quot;testAction2&quot;

When I drop extends Actions in line export interface ExampleActions extends Actions { then it works as expected.

How to make it happen without droping it?

Why I need it?

I want to some class knows allowed parameteres in some method call, like this:

class ActionsManager&lt;CALLABLE_ACTIONS extends Actions&gt; {
    callAction(name: keyof CALLABLE_ACTIONS) {
        //...
    }
}

const am = new ActionsManager&lt;ExampleActions&gt;();

am.callAction(&quot;testAction&quot;); //good
am.callAction(&quot;testAction2&quot;); //good
am.callAction(&quot;anotherAction&quot;); //wrong, there is no property &quot;anotherAction&quot; in &quot;ExampleActions&quot; interface

May be related:

https://github.com/microsoft/TypeScript/issues/24274

https://github.com/DanielXMoore/Civet/issues/271

looks like it isn't possible right now? But there is keyword satisfies that some people consider to introduce to Typescript syntax.

答案1

得分: 1

Ideally the time to pull this information out of ExampleActions is before you mix it with Actions via interface extension and before these known keys are absorbed into string:

interface BaseExampleActions {
    testAction: {}
    testAction2: {}
}

interface ExampleActions extends Actions, BaseExampleActions { }

type Test = keyof BaseExampleActions;
const a: Test = "gfhgfh"; // error!

But if you already have ExampleActions defined that way and really need to extract the known keys and ignore the index signature, then you can use key remapping in mapped types to do it:

type KnownKeys<T> = keyof {
    [K in keyof T as {} extends Record<K, any> ? never : K]: any
}

Here what we're doing is checking each key K in keyof T to see if it's one of the valid index signature key types like string, number, symbol or a pattern template literal type such as `abc${string}def`. The only way I know of to do that is to test for whether an object with that key type is allowed to be empty. A literal type like "a" fails that test, because {} cannot be assigned to {a: any}. But a type like string passes, because {} can be assigned to {[k: string]: any}.

Anyway, let's test it out:

type Test = KnownKeys<ExampleActions>;
// type Test = "testAction" | "testAction2"

Looks good. KnownKeys<ExampleActions> returns just "testAction" | "testAction2" and the string part is discarded.

英文:

Ideally the time to pull this information out of ExampleActions is before you mix it with Actions via interface extension and before these known keys are absorbed into string:

interface BaseExampleActions {
    testAction: {}
    testAction2: {}
}

interface ExampleActions extends Actions, BaseExampleActions { }

type Test = keyof BaseExampleActions;
const a: Test = &quot;gfhgfh&quot;; // error!

But if you already have ExampleActions defined that way and really need to extract the known keys and ignore the index signature, then you can use key remapping in mapped types to do it:

type KnownKeys&lt;T&gt; = keyof {
    [K in keyof T as {} extends Record&lt;K, any&gt; ? never : K]: any
}

Here what we're doing is checking each key K in keyof T to see if it's one of the valid index signature key types like string, number, symbol or a pattern template literal type such as `abc${string}def`. The only way I know of to do that is to test for whether an object with that key type is allowed to be empty. A literal type like &quot;a&quot; fails that test, because {} cannot be assigned to {a: any}. But a type like string passes, because {} can be assigned to {[k: string]: any}.

Anyway, let's test it out:

type Test = KnownKeys&lt;ExampleActions&gt;;
// type Test = &quot;testAction&quot; | &quot;testAction2&quot;

Looks good. KnownKeys&lt;ExampleActions&gt; returns just &quot;testAction&quot; | &quot;testAction2&quot; and the string part is discarded.

Playground link to code

huangapple
  • 本文由 发表于 2023年3月7日 20:18:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/75661880.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定