Repository: netlify/build
Pull request #3850 (2021-11-16 08:50:11)

fix: allow `interface`s to be used as Plugins generics inputs
#### Summary

Fixes https://github.com/netlify/build/issues/3838

Allows users of `@netlify/build` TypeScript definitions to use `interface`s as their inputs:
```ts
interface InputsInterface {
  someProp: string
}

const testSpecificInputsInterface: OnPreBuild<InputsInterface> = function ({ inputs }) {
  expectType<string>(inputs.someProp)
  expectError(inputs.otherTestVar)
}
```
and not just `type`s - which already work today due to them having implicit index signatures.

This is preferable since `interface`s are more prevalent than `type`s, and are the [suggested go-to](https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections) over `type`s - but more importantly - `interface`s are the go-to for the community at large, so there's no reason not to let people use it, let alone leak implementation details out from the package on why they're not allowed.

The fix suggested here is not the best out there probably, but I gave some more detail in https://github.com/netlify/build/issues/3838#issuecomment-969313750 on why I think this is _good enough_, at least for now/until we come up with a better implementation - which will be non-breaking anyway.

---

For us to review and ship your PR efficiently, please perform the following steps:

- [x] Open a [bug/issue](https://github.com/netlify/build/issues/new/choose) before writing your code 🧑‍💻. This ensures we can discuss the changes and get feedback from everyone that should be involved. If you\`re fixing a typo or something that\`s on fire 🔥 (e.g. incident related), you can skip this step.
- [x] Read the [contribution guidelines](../CONTRIBUTING.md) 📖. This ensures your code follows our style guide and
      passes our tests.
- [x] Update or add tests (if any source code was changed or added) 🧪
- [x] Update or add documentation (if features were changed or added) 📝
- [ ] Make sure the status checks below are successful ✅


Patch:
diff --git a/packages/build/types/config/inputs.d.ts b/packages/build/types/config/inputs.d.ts
index 8b7be5ad6..64202e03c 100644
--- a/packages/build/types/config/inputs.d.ts
+++ b/packages/build/types/config/inputs.d.ts
@@ -1,3 +1,7 @@
 import { JSONValue } from '../utils/json_value'
 
-export type PluginInputs = Partial<Record<string, JSONValue>>
+// Helper type to be used as a workaround for the fact that `interface`s don't have implicit
+// index signatures: https://github.com/microsoft/TypeScript/issues/15300
+export type StringKeys<TObject extends object> = keyof TObject & string
+
+export type PluginInputs<Keys extends string = string> = Partial<Record<Keys, JSONValue>>
diff --git a/packages/build/types/netlify_event_handler.d.ts b/packages/build/types/netlify_event_handler.d.ts
index 7a8787f9c..c5c47081c 100644
--- a/packages/build/types/netlify_event_handler.d.ts
+++ b/packages/build/types/netlify_event_handler.d.ts
@@ -1,19 +1,29 @@
-import { PluginInputs } from './config/inputs'
+import { PluginInputs, StringKeys } from './config/inputs'
 import { NetlifyPluginOptions } from './netlify_plugin_options'
 
 interface NetlifyEventHandler<PluginOptions extends NetlifyPluginOptions = NetlifyPluginOptions> {
   (options: PluginOptions): void | Promise<void>
 }
 
-export type OnPreBuild<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<NetlifyPluginOptions<TInputs>>
-export type OnBuild<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<NetlifyPluginOptions<TInputs>>
-export type OnPostBuild<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<
+// To allow interfaces to be used as generics, since they lack implicit index signatures, we have to do some type shenanigans
+// to get TypeScript to behave as we want - only letting the keys of `TInputs` through, and thus not requiring a full index
+// signature on `TInputs`.
+// Related issues: https://github.com/microsoft/TypeScript/issues/15300, https://github.com/netlify/build/issues/3838
+export type OnPreBuild<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
   NetlifyPluginOptions<TInputs>
 >
-export type OnError<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<
+export type OnBuild<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
+  NetlifyPluginOptions<TInputs>
+>
+export type OnPostBuild<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
+  NetlifyPluginOptions<TInputs>
+>
+export type OnError<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
   NetlifyPluginOptions<TInputs> & { error: Error }
 >
-export type OnSuccess<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<NetlifyPluginOptions<TInputs>>
-export type OnEnd<TInputs extends PluginInputs = PluginInputs> = NetlifyEventHandler<
+export type OnSuccess<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
+  NetlifyPluginOptions<TInputs>
+>
+export type OnEnd<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> = NetlifyEventHandler<
   NetlifyPluginOptions<TInputs> & { error?: Error }
 >
diff --git a/packages/build/types/netlify_plugin_options.d.ts b/packages/build/types/netlify_plugin_options.d.ts
index 21f237755..2505eed70 100644
--- a/packages/build/types/netlify_plugin_options.d.ts
+++ b/packages/build/types/netlify_plugin_options.d.ts
@@ -1,10 +1,10 @@
-import { PluginInputs } from './config/inputs'
+import { PluginInputs, StringKeys } from './config/inputs'
 import { NetlifyConfig } from './config/netlify_config'
 import { NetlifyPluginConstants } from './netlify_plugin_constants'
 import { NetlifyPluginUtils } from './options/netlify_plugin_utils'
 import { JSONValue } from './utils/json_value'
 
-export interface NetlifyPluginOptions<TInputs extends PluginInputs = PluginInputs> {
+export interface NetlifyPluginOptions<TInputs extends PluginInputs<StringKeys<TInputs>> = PluginInputs> {
   /**
    * If your plugin requires additional values from the user, you can specify these requirements in an `inputs` array in the plugin’s [`manifest.yml` file](https://docs.netlify.com/configure-builds/build-plugins/create-plugins/#anatomy-of-a-plugin).
    */


Test patch:
diff --git a/packages/build/test-d/config/inputs.ts b/packages/build/test-d/config/inputs.ts
index 7ba8f37ab..ef82e75cc 100644
--- a/packages/build/test-d/config/inputs.ts
+++ b/packages/build/test-d/config/inputs.ts
@@ -1,5 +1,5 @@
-import { NetlifyPlugin, OnPreBuild } from '@netlify/build'
-import { expectAssignable, expectType, expectError } from 'tsd'
+import { OnPreBuild } from '@netlify/build'
+import { expectAssignable, expectError, expectType } from 'tsd'
 
 import { JSONValue } from '../../types/utils/json_value'
 
@@ -11,3 +11,12 @@ const testSpecificInputs: OnPreBuild<{ testVar: boolean }> = function ({ inputs
   expectType<boolean>(inputs.testVar)
   expectError(inputs.otherTestVar)
 }
+
+interface InputsInterface {
+  testVar: string
+}
+
+const testSpecificInputsInterface: OnPreBuild<InputsInterface> = function ({ inputs }) {
+  expectType<string>(inputs.testVar)
+  expectError(inputs.otherTestVar)
+}