简体   繁体   English

TypeScript - 基于通用枚举或字符串列表限制抽象成员的键

[英]TypeScript - Restrict keys of an abstract member based on generic enum or string list

I'm having trouble figuring out how to even find the right words to explain what I'm trying to do, so apologies if I mix up terminology.我什至不知道如何找到正确的词来解释我正在尝试做的事情,所以如果我混淆了术语,请道歉。 What I'm trying to do is restrict the keys of an object / interface to a list of keys belonging to an enum (or string[] list).我想要做的是将对象/接口的键限制为属于枚举(或 string[] 列表)的键列表。 This is already possible at a basic level, like so:这在基本层面已经是可能的,如下所示:

enum RestrictKeysTo {
    name = 'name',
    age = 'age'
}
type Person = {
    [K in RestrictKeysTo]?: string | number;
}
const phil: Person = {
    // Allowed
    age: 24,
    name: 'Phillip'
    // Throws error
    occupation: 'developer'
};

This is great!这很棒! However, I want to use this in a more flexible way, where the list of accepted keys can be passed in via an interface or class, and the list can be used to restrict keys elsewhere.但是,我想以更灵活的方式使用它,其中可以通过接口或类传入已接受键的列表,并且可以使用该列表来限制其他地方的键。 The specific end goal I'm looking for is something like this:我正在寻找的具体最终目标是这样的:


abstract class BaseClass {
    public abstract keys: string[];
    // How do I restrict ages to be keyed by keys?
    public abstract ages: //...
}

class Final extends BaseClass {
    public keys = ['joe', 'ralph'];
    public ages = {
        joe: 24,
        ralph: 50
    }
}

I'm guessing that if a solution exists, it involves passing generics to the base class, but I'm having trouble working out the specifics... for example, this is the closest I can seem to get ( working ):我猜如果存在解决方案,它涉及将泛型传递给基类,但我无法确定具体细节......例如,这是我似乎得到的最接近的(工作):

abstract class BaseClass<T extends string> {
    public abstract ages: {
        [K in T]: number
    }
}

type Keys = 'joe' | 'ralph' | 'betty'

class Final extends BaseClass<Keys> {
    public ages = {
        joe: 24,
        ralph: 50,
        betty: 32
    };
}

However, this is less than ideal, as I want to be able to reuse the keys across base and filled in (abstract) methods.但是,这不太理想,因为我希望能够跨基类重用密钥并填充(抽象)方法。 I was thinking I would pass it via the constructor, but I'm running into two issues:我以为我会通过构造函数传递它,但我遇到了两个问题:

  • It requires the dev to retype the union as an array它要求开发人员将联合重新键入为数组
  • I can't get it to type guard quite right我不能让它完全正确地打字守卫

Here is a non-functional example, where I might be close:这是一个非功能性示例,我可能很接近:

type KeyArr<T extends string> = {
    [K in T]?: keyof T;
}

abstract class BaseClass<T extends string> {
    public keys: KeyArr<T>;
    constructor(keys: KeyArr<T>) {
        this.keys = keys;
    }
    public abstract ages: {
        [K in T]?: number
    }
}

type Keys = 'joe' | 'ralph' | 'betty'

class Final extends BaseClass<Keys> {
    constructor() {
        // This should ideally throw an error, since it is missing 'betty', and 'joe' does not belong to Keys
        // Not working - says nothing in common with KeyArr
        super(['joe', 'ralph', 'joe'])
    }
    public ages = {
        joe: 24,
        ralph: 50,
        betty: 24
    };
}

To be completely honest, for some reason I have a mental block with TS generics - so it is very likely I am missing something basic here.老实说,出于某种原因,我对 TS 泛型有一个心理障碍——所以很可能我在这里遗漏了一些基本的东西。 Any help is appreciated!任何帮助表示赞赏!

Playing with your code shows following solution to be working correctly and it should match your requirements.使用您的代码显示以下解决方案可以正常工作,并且应该符合您的要求。

Solution with enum枚举的解决方案

abstract class BaseClass<T extends string> {
    public abstract keys: T[];
    public abstract ages: {
        [K in T]?: number
    }
}

enum Keys {
    Joe = "Joe",
    Ralph = "Ralph",
    Betty = "Betty"
}

class Final extends BaseClass<Keys> {
    public keys = [Keys.Betty, Keys.Joe, Keys.Ralph];
    public ages = {
        [Keys.Betty]: 24
    };
}

const final = new Final();
const a = final.ages[Keys.Betty] // number

As you can see I did few things: 1. I am using enum instead of string literal 2. I removed constructor, as in final class you were not using argument of it 3. I changed types of keys into simple T[]如您所见,我做了几件事:1. 我使用的是枚举而不是字符串文字 2. 我删除了构造函数,因为在最终类中您没有使用它的参数 3. 我将键的类型更改为简单的T[]

Now why I did those changes.现在为什么我做了这些改变。

  1. Problem with string literal was - in extending class passing ['betty', 'ralph'] was inferencing it as string[], to fix it I needed to do - ['betty' as const, 'ralph' as const].字符串文字的问题是 - 在扩展类传递 ['betty', 'ralph'] 时将其推断为 string[],为了修复它,我需要做 - ['betty' as const, 'ralph' as const]。 As I wanted to avoid that, I have chosen enum.由于我想避免这种情况,我选择了枚举。

  2. I removed constructor as your implementation shows you want to define keys inside class, and not to pass them from outside.我删除了构造函数,因为您的实现表明您想要在类内部定义键,而不是从外部传递它们。

  3. KeyArr was no difference from simple T[] KeyArr与简单的T[]没有区别

Solution with string literal type字符串文字类型的解决方案

Below implementation with string literal form comparison:下面使用字符串文字形式比较实现:

abstract class BaseClass<T extends string> {
    public abstract keys: T[];
    public abstract ages: {
        [K in T]?: number
    }
}

type Keys = 'joe' | 'ralph' | 'betty'

class Final extends BaseClass<Keys> {
    public keys = ['betty' as const, 'joe' as const]; // as const because inference showing string
    public ages = {
        'betty': 24
    };
}

const final = new Final();
const a = final.ages.betty // number

Both solutions look valid.两种解决方案看起来都有效。

About exhaustiveness.关于穷举。

Boths solution given by me allows on putting array of not all keys.我给出的两个解决方案都允许放置并非所有键的数组。 It is done because your type for ages not requires value for every key - {[K in T]?: number} means its a partial record.这样做是因为您的ages类型不需要每个键的值 - {[K in T]?: number}表示它的部分记录。 In my opinion the whole would have a sense if keys would not be exhaustive but ages would be.在我看来,如果keys不是详尽无遗而年龄是详尽无遗的,那么整体就会有意义。 Consider:考虑:

abstract class BaseClass<T extends string> {
    public abstract keys: T[];
    public abstract ages: {
        [K in T]: number // pay attention removed ?
    }
}

type Keys = 'joe' | 'ralph' | 'betty'

class Final extends BaseClass<Keys> {
    public keys = ['betty' as const, 'joe' as const]; // as const because inference showing string
    public ages = {
        'betty': 24 // error total record of all keys needs to be provided
    };
}

This difference means that in our Final class we can define which keys we are requiring for ages to be provided.这种差异意味着在我们的 Final 类中,我们可以定义我们需要为年龄提供哪些键。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM