From 27c8fc0342bb6a5c292d1ad145d219f75e8d9e21 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Sun, 12 Jul 2026 17:08:43 +0900 Subject: [PATCH 1/3] add typescript api checks for provider provided value --- .../graphql-modules/tests/provider-types.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/graphql-modules/tests/provider-types.ts diff --git a/packages/graphql-modules/tests/provider-types.ts b/packages/graphql-modules/tests/provider-types.ts new file mode 100644 index 0000000000..3c7f1f6a3d --- /dev/null +++ b/packages/graphql-modules/tests/provider-types.ts @@ -0,0 +1,66 @@ +import { + createApplication, + createModule, + InjectionToken, + Provider, + testkit, +} from 'graphql-modules'; + +const StringToken = new InjectionToken('string'); + +// @ts-expect-error injection tokens retain their value type +const incorrectToken: InjectionToken = StringToken; + +void incorrectToken; + +class Service { + readonly kind = 'service'; +} + +class OtherService { + readonly kind = 'other-service'; +} + +createApplication({ + modules: [], + providers: [{ provide: StringToken, useValue: 'value' }], +}); + +createApplication({ + modules: [], + // @ts-expect-error useValue must match the token value type + providers: [{ provide: StringToken, useValue: 123 }], +}); + +createApplication({ + modules: [], + providers: [{ provide: Service, useValue: new Service() }], +}); + +createApplication({ + modules: [], + // @ts-expect-error useValue must match the class token type + providers: [{ provide: Service, useValue: new OtherService() }], +}); + +const widenedProviders: Provider[] = [{ provide: StringToken, useValue: 123 }]; + +createApplication({ modules: [], providers: widenedProviders }); + +createModule({ + id: 'provider-types', + typeDefs: [], + providers: [{ provide: StringToken, useValue: 'value' }], +}); + +createModule({ + id: 'invalid-provider-types', + typeDefs: [], + // @ts-expect-error useValue must match the token value type + providers: [{ provide: StringToken, useValue: 123 }], +}); + +testkit.testInjector([{ provide: StringToken, useValue: 'value' }]); + +// @ts-expect-error useValue must match the token value type +testkit.testInjector([{ provide: StringToken, useValue: 123 }]); From ae201f250a4254e588f395b5d3b39d4cda24b721 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Sun, 12 Jul 2026 17:19:55 +0900 Subject: [PATCH 2/3] improve type safety of provided values --- .../src/application/application.ts | 7 ++--- .../graphql-modules/src/application/types.ts | 7 +++-- packages/graphql-modules/src/di/providers.ts | 27 ++++++++++++++++++- packages/graphql-modules/src/module/module.ts | 5 +++- packages/graphql-modules/src/module/types.ts | 7 +++-- .../src/testing/test-injector.ts | 6 +++-- .../graphql-modules/tests/provider-types.ts | 9 +++++++ 7 files changed, 57 insertions(+), 11 deletions(-) diff --git a/packages/graphql-modules/src/application/application.ts b/packages/graphql-modules/src/application/application.ts index f5d7fb25be..314d8438cc 100644 --- a/packages/graphql-modules/src/application/application.ts +++ b/packages/graphql-modules/src/application/application.ts @@ -1,5 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { + Provider, ReflectiveInjector, onlySingletonProviders, onlyOperationProviders, @@ -58,9 +59,9 @@ export interface InternalAppContext { * }) * ``` */ -export function createApplication( - applicationConfig: ApplicationConfig -): Application { +export function createApplication< + const TProviders extends Provider[] = Provider[], +>(applicationConfig: ApplicationConfig): Application { function applicationFactory(cfg?: ApplicationConfig): Application { const config = cfg || applicationConfig; const providers = diff --git a/packages/graphql-modules/src/application/types.ts b/packages/graphql-modules/src/application/types.ts index 40bc990fba..0941de195a 100644 --- a/packages/graphql-modules/src/application/types.ts +++ b/packages/graphql-modules/src/application/types.ts @@ -6,6 +6,7 @@ import { ExecutionResult, } from 'graphql'; import type { Provider, Injector } from '../di'; +import type { ValidatedProviders } from '../di/providers'; import type { Resolvers, Module, MockedModule } from '../module/types'; import type { MiddlewareMap } from '../shared/middleware'; import type { ApolloRequestContext } from './apollo'; @@ -120,7 +121,7 @@ export interface OperationController { * @api * Application's configuration object. Represents the first argument of `createApplication` function. */ -export interface ApplicationConfig { +export interface ApplicationConfig { /** * A list of GraphQL Modules */ @@ -128,7 +129,9 @@ export interface ApplicationConfig { /** * A list of Providers - read the ["Providers and Tokens"](./di/providers) chapter. */ - providers?: Provider[] | (() => Provider[]); + providers?: + | ValidatedProviders + | (() => ValidatedProviders); /** * A map of middlewares - read the ["Middlewares"](./advanced/middlewares) chapter. */ diff --git a/packages/graphql-modules/src/di/providers.ts b/packages/graphql-modules/src/di/providers.ts index 6d7f93f01a..38e6b7b6a9 100644 --- a/packages/graphql-modules/src/di/providers.ts +++ b/packages/graphql-modules/src/di/providers.ts @@ -4,7 +4,11 @@ export const Type = Function; /// @ts-ignore export class InjectionToken { - constructor(private _desc: string) {} + private readonly _desc: string & { readonly __type?: T }; + + constructor(desc: string) { + this._desc = desc as string & { readonly __type?: T }; + } toString(): string { return `InjectionToken ${this._desc}`; @@ -54,6 +58,27 @@ export type Provider = | ClassProvider | FactoryProvider; +type TokenValue = TToken extends + | InjectionToken + | Type + | AbstractType + ? TValue + : never; + +type ValidateValueProvider = TProvider extends { + provide: infer TToken; + useValue: infer TValue; +} + ? [TValue] extends [TokenValue] + ? TProvider + : never + : TProvider; + +export type ValidatedProviders = + TProviders & { + [TIndex in keyof TProviders]: ValidateValueProvider; + }; + export interface ProviderOptions { scope?: Scope; executionContextIn?: Array; diff --git a/packages/graphql-modules/src/module/module.ts b/packages/graphql-modules/src/module/module.ts index 1c73eac47a..c852a234e2 100644 --- a/packages/graphql-modules/src/module/module.ts +++ b/packages/graphql-modules/src/module/module.ts @@ -1,4 +1,5 @@ import { moduleFactory } from './factory'; +import { Provider } from '../di'; import { ModuleConfig } from './types'; /** @@ -21,6 +22,8 @@ import { ModuleConfig } from './types'; * }); * ``` */ -export function createModule(config: ModuleConfig) { +export function createModule( + config: ModuleConfig +) { return moduleFactory(config); } diff --git a/packages/graphql-modules/src/module/types.ts b/packages/graphql-modules/src/module/types.ts index 4b3149a75e..de78237d14 100644 --- a/packages/graphql-modules/src/module/types.ts +++ b/packages/graphql-modules/src/module/types.ts @@ -3,6 +3,7 @@ import { ModuleFactory } from './factory'; import { ID, Plural } from '../shared/types'; import { ModuleMetadata } from './metadata'; import { Provider } from '../di'; +import type { ValidatedProviders } from '../di/providers'; import { MiddlewareMap } from '../shared/middleware'; export type TypeDefs = Plural; @@ -12,7 +13,7 @@ export type Resolvers = Plural>; * @api * Module's configuration object. Represents the first argument of `createModule` function. */ -export interface ModuleConfig { +export interface ModuleConfig { /** * Unique identifier of a module */ @@ -36,7 +37,9 @@ export interface ModuleConfig { /** * A list of Providers - read the ["Providers and Tokens"](./di/providers) chapter. */ - providers?: Provider[] | (() => Provider[]); + providers?: + | ValidatedProviders + | (() => ValidatedProviders); } export interface Module { diff --git a/packages/graphql-modules/src/testing/test-injector.ts b/packages/graphql-modules/src/testing/test-injector.ts index 0eab2e3f5a..ee1ee2087a 100644 --- a/packages/graphql-modules/src/testing/test-injector.ts +++ b/packages/graphql-modules/src/testing/test-injector.ts @@ -1,9 +1,11 @@ import { Injector, ReflectiveInjector } from '../di/injector'; -import { Provider, TypeProvider } from '../di/providers'; +import { Provider, TypeProvider, ValidatedProviders } from '../di/providers'; import { CONTEXT } from '../application/tokens'; import { readInjectableMetadata } from '../di/metadata'; -export function testInjector(providers: Provider[]): Injector { +export function testInjector( + providers: ValidatedProviders +): Injector { const resolvedProviders = ReflectiveInjector.resolve([ { provide: CONTEXT, useValue: {} }, ...providers, diff --git a/packages/graphql-modules/tests/provider-types.ts b/packages/graphql-modules/tests/provider-types.ts index 3c7f1f6a3d..2947ba31c8 100644 --- a/packages/graphql-modules/tests/provider-types.ts +++ b/packages/graphql-modules/tests/provider-types.ts @@ -7,6 +7,9 @@ import { } from 'graphql-modules'; const StringToken = new InjectionToken('string'); +declare const StringOrNumberToken: + | InjectionToken + | InjectionToken; // @ts-expect-error injection tokens retain their value type const incorrectToken: InjectionToken = StringToken; @@ -32,6 +35,12 @@ createApplication({ providers: [{ provide: StringToken, useValue: 123 }], }); +createApplication({ + modules: [], + // @ts-expect-error useValue must be valid for every token in the union + providers: [{ provide: StringOrNumberToken, useValue: false }], +}); + createApplication({ modules: [], providers: [{ provide: Service, useValue: new Service() }], From c3446746cbb6fd1dfccad8a6b96eeaee85d2fa83 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Sun, 12 Jul 2026 17:58:56 +0900 Subject: [PATCH 3/3] add `defineProviders` helper for better validation for library users --- .changeset/strict-providers.md | 30 +++++++++++++++++++ packages/graphql-modules/src/di/index.ts | 1 + packages/graphql-modules/src/di/providers.ts | 9 ++++++ packages/graphql-modules/src/index.ts | 1 + .../graphql-modules/tests/provider-types.ts | 16 ++++++++++ website/src/content/di/providers.mdx | 28 +++++++++++++++++ 6 files changed, 85 insertions(+) create mode 100644 .changeset/strict-providers.md diff --git a/.changeset/strict-providers.md b/.changeset/strict-providers.md new file mode 100644 index 0000000000..9b6cd62f7c --- /dev/null +++ b/.changeset/strict-providers.md @@ -0,0 +1,30 @@ +--- +'graphql-modules': minor +--- + +Add `defineProviders` to preserve type-safe provider inference for separately declared provider arrays and provider factories. + +Inline providers now verify that `useValue` matches its `InjectionToken` or class token type, including union tokens. Explicit `Provider[]` annotations remain supported but opt out of this inference. + +```ts +const ApiKey = new InjectionToken('api-key') + +createApplication({ + modules: [], + providers: [{ provide: ApiKey, useValue: 'my-api-key' }] +}) +``` + +Use `defineProviders` when providers are declared separately or returned from a provider factory: + +```ts +const providers = defineProviders([{ provide: ApiKey, useValue: 'my-api-key' }]) + +createApplication({ modules: [], providers }) + +createApplication({ + modules: [], + providers: () => + defineProviders([{ provide: ApiKey, useValue: 'my-api-key' }]) +}) +``` diff --git a/packages/graphql-modules/src/di/index.ts b/packages/graphql-modules/src/di/index.ts index 18ff2ddd01..d7ec6b65b3 100644 --- a/packages/graphql-modules/src/di/index.ts +++ b/packages/graphql-modules/src/di/index.ts @@ -10,6 +10,7 @@ export { Factory, FactoryProvider, TypeProvider, + defineProviders, ProviderOptions, Scope, onlySingletonProviders, diff --git a/packages/graphql-modules/src/di/providers.ts b/packages/graphql-modules/src/di/providers.ts index 38e6b7b6a9..f5ddb5b6dd 100644 --- a/packages/graphql-modules/src/di/providers.ts +++ b/packages/graphql-modules/src/di/providers.ts @@ -79,6 +79,15 @@ export type ValidatedProviders = [TIndex in keyof TProviders]: ValidateValueProvider; }; +/** + * Preserves provider inference for separately declared provider arrays. + */ +export function defineProviders( + providers: ValidatedProviders +): TProviders { + return providers; +} + export interface ProviderOptions { scope?: Scope; executionContextIn?: Array; diff --git a/packages/graphql-modules/src/index.ts b/packages/graphql-modules/src/index.ts index 2ecd92fd5e..ac8735d26d 100644 --- a/packages/graphql-modules/src/index.ts +++ b/packages/graphql-modules/src/index.ts @@ -22,6 +22,7 @@ export { ClassProvider, ValueProvider, TypeProvider, + defineProviders, forwardRef, InjectionToken, Scope, diff --git a/packages/graphql-modules/tests/provider-types.ts b/packages/graphql-modules/tests/provider-types.ts index 2947ba31c8..0e8f63a1fb 100644 --- a/packages/graphql-modules/tests/provider-types.ts +++ b/packages/graphql-modules/tests/provider-types.ts @@ -1,6 +1,7 @@ import { createApplication, createModule, + defineProviders, InjectionToken, Provider, testkit, @@ -56,6 +57,21 @@ const widenedProviders: Provider[] = [{ provide: StringToken, useValue: 123 }]; createApplication({ modules: [], providers: widenedProviders }); +const declaredProviders = defineProviders([ + { provide: StringToken, useValue: 'value' }, +]); + +createApplication({ modules: [], providers: declaredProviders }); + +// @ts-expect-error useValue must match the token value type +defineProviders([{ provide: StringToken, useValue: 123 }]); + +createApplication({ + modules: [], + providers: () => + defineProviders([{ provide: StringToken, useValue: 'value' }]), +}); + createModule({ id: 'provider-types', typeDefs: [], diff --git a/website/src/content/di/providers.mdx b/website/src/content/di/providers.mdx index cd9f382ddb..d22494bfe9 100644 --- a/website/src/content/di/providers.mdx +++ b/website/src/content/di/providers.mdx @@ -11,6 +11,34 @@ The building blocks of DI are **Provider**(s) and **InjectionToken**(s). Dependency Injection is an abstraction over actual objects and values. + + In direct, inline provider arrays, GraphQL Modules checks that a `useValue` + matches its typed `InjectionToken` or class token. Explicitly widening an + array to `Provider[]` erases that information. Use `defineProviders` for a + separately declared array or provider factory. + + +```ts +const ApiKey = new InjectionToken('api-key') + +createApplication({ + modules: [], + providers: [ + // TypeScript error: number is not assignable to string. + { provide: ApiKey, useValue: 123 } + ] +}) +``` + +```ts +const providers = defineProviders([{ provide: ApiKey, useValue: 'my-api-key' }]) + +createApplication({ + modules: [], + providers +}) +``` + There are three kinds of providers: - [Class provider](#class) - creates an instance of a class