简体   繁体   English

打字稿:如何在没有运行时工件的情况下制作通用版本的编译时集?

[英]Typescript: How to make a generic version of compile-time set without runtime artifacts?

I am trying to make Typescript warn me about incorrect usages of the API that we have (the errors I expect to see are marked):我试图让 Typescript 警告我关于我们所拥有的 API 的不正确用法(我希望看到的错误被标记):

interface Icon { src: string; }

interface IconSet {
  [iconName: string]: Icon;
}

type IconRegistry<T> = {
  [K in keyof T]: Icon;
};

function asIconRegistry<T extends IconSet>(iconSet: T): IconRegistry<T> {
  return iconSet as any;
}

type UiBuilder = <P, S extends IconRegistry<P>>(iconRegistry: S) => {
  withIcon<K extends keyof S>(iconName: K): null,
};

const listIcons = asIconRegistry({
  sort: { src: 'sort.svg' }, // Expected to be OK
  email: { source: 'email.svg' }, // Expected to ERR
});

const profileIcons = asIconRegistry({
  security: { src: 'security.svg' }, // Expected to be OK
  email: { src: 'email.svg' }, // Expected to be OK
});

function buildUi(builder: UiBuilder) {
  builder(profileIcons).withIcon('security'); // Expected to be OK
  builder(profileIcons).withIcon('bold'); // Expected to ERR
}

It works great, as can be seen here , except for one small detail - the asIconRegistry function is an actual runtime artifact, which is non-ideal.它工作得很好,可以在这里看到,除了一个小细节 - asIconRegistry函数是一个实际的运行时工件,这是不理想的。

What I've tried:我试过的:

Typing the variables manually:手动输入变量:

const listIcons2: IconRegistry<{ sort: Icon, email: Icon }> = {
  sort: { src: 'sort.svg' }, // Expected to be OK
  email: { source: 'email.svg' }, // Expected to ERR
};

This works, but is too verbose - the usefulness of generics is kinda lost.这有效,但太冗长了 - 泛型的用处有点丢失。 In addition to that this becomes harder to maintain as the list grows in size and complexity, and it is harder to persuade consumers of this API to type their things this way.除此之外,随着列表的大小和复杂性的增加,这变得更难维护,并且更难说服此 API 的消费者以这种方式键入他们的内容。

How can I achieve the same level of type safety without runtime artifacts and the above verbosity?如何在没有运行时工件和上述冗长的情况下实现相同级别的类型安全?

Okay, let's clean things up a bit.好吧,让我们清理一下。 Your generics are a bit wider than necessary;你的泛型比必要的要宽一些; mostly you are using full object types when you only actually care about their key names.大多数情况下,当您真正关心它们的键名时,您正在使用完整的对象类型。 Here's my changes:这是我的更改:

// Icon is the same
interface Icon { src: string; }

// you just need key names here (K), not a full object type (T)
type IconRegistry<K extends string> = Record<K, Icon>

// you don't really need IconSet, but for convenience, here it is
type IconSet = IconRegistry<string>

// the old P and S didn't do anything except determine keys
// let's just use those keys (K) directly
type UiBuilder = <K extends string>(iconRegistry: IconRegistry<K>) => {
  withIcon(iconName: K): null,
};

So now you're ready to create and use some IconSet s.所以现在您已准备好创建和使用一些IconSet

I don't really understand why you think it's important to have zero runtime impact from your type checking (an identity function call is probably fairly low impact especially with modern JavaScript engines ), but I like a challenge.我真的不明白为什么您认为类型检查对运行时的影响为零很重要(身份函数调用的影响可能相当低,尤其是对于现代 JavaScript 引擎),但我喜欢挑战。 You want to verify that listIcons and profileIcons are valid IconSet s at compile time without anything being emitted into the JavaScript.您想在编译时验证listIconsprofileIcons是有效的IconSet ,而不IconSet任何内容发送到 JavaScript。

How about this:这个怎么样:

type VerifyIconRegistry<T extends IconSet> = any

You need to pass a valid IconSet as the parameter to VerifyIconRegistry<> .您需要将有效的IconSet作为参数传递给VerifyIconRegistry<> Let's see it in action with the invalid listIcons :让我们用无效的listIcons看看它的作用:

const listIcons = {
  sort: { src: 'sort.svg' },
  email: { source: 'email.svg' } 
};

declare var witness: 
  VerifyIconRegistry<typeof listIcons> // ERROR
  // Property 'src' is missing in type '{ source: string; }'

There's the error.有错误。 typeof listIcons is not a valid IconSet . typeof listIcons不是有效的IconSet Note declare var witness doesn't emit any JavaScript.注意declare var witness不会发出任何 JavaScript。 It does add a variable named witness into ambient scope here at compile time... give it any name you don't need, and you can reuse it since var s can be redeclared.它确实在编译时将一个名为witness的变量添加到环境作用域中......给它任何你不需要的名字,你可以重用它,因为可以重新声明var s。

Now for the valid profileIcons :现在对于有效的profileIcons

const profileIcons = {
  security: { src: 'security.svg' },
  email: { src: 'email.svg' }
};

declare var witness:
  VerifyIconRegistry<typeof profileIcons> // OKAY

That one worked (and we reused the witness name).那个有效(我们重用了witness名字)。

Finally, let's make sure we didn't break buildUi() :最后,让我们确保我们没有破坏buildUi()

function buildUi(builder: UiBuilder) {
  builder(profileIcons).withIcon('security'); // OKAY
  builder(profileIcons).withIcon('bold'); // ERROR
  // Argument of type '"bold"' is not assignable 
  // to parameter of type '"security" | "email"'.
}

Looks good.看起来挺好的。 Hope that helps;希望有所帮助; good luck!祝你好运!

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

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