Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Introduce Type-safe Token Providing #55555

Open
Char2sGu opened this issue Apr 25, 2024 · 7 comments
Open

[Feature] Introduce Type-safe Token Providing #55555

Char2sGu opened this issue Apr 25, 2024 · 7 comments
Labels
area: core Issues related to the framework runtime core: di cross-cutting: types feature Issue that requests a new feature
Milestone

Comments

@Char2sGu
Copy link

Char2sGu commented Apr 25, 2024

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

The current object-literal-based providing syntax cannot ensure:

  1. The type of the provided value is compatible with the token.
  2. The multi: true option is not missing or mistakenly added

For libraries, in order to ensure type-safe providing, it has been a common pattern to create provideXxx helpers. However, in daily application development, the providng experience is still non-type-safe.

Since all the ProviderTokens carry type information, it is possible to create framework-level providing helpers to ensure type safety.

I have created such helper functions and fully adopted it in all my projects.
I would like to create a PR to introduce these helpers to @angular/core to enable a safer providing experience for all Angular developers.

Please see below for details.

Proposed solution

Signatures

function provide<T, U extends T>(config: ProvideConfig<T, U>): Provider;
function provideMulti<T>(config: ProvideMultiConfig<T>): Provider;

Usage

provide({ token: TitleStrategy, useClass: AppTitleStrategy });
provide({ token: TitleStrategy, useExisting: AppTitleStrategy });
// `AppTitleStrategy` have to be assignable to `TitleStrategy`.

declare const SNACKBAR_CONFIG: InjectionToken<SnackbarConfig>;
provide({ token: SNACKBAR_CONFIG, useValue: { ... } });
// Type of `useValue` is inferred as `SnackbarConfig`, enabling autocomplete.

declare const BREAKPOINTS: InjectionToken<BreakpointMap>
provide({ token: BREAKPOINTS, useFactory: (observer = inject(BreakpointObserver)) => observer.observe() });
// Return type of the factory has to match `BreakpointMap`

provide({ token: APP_INITIALIZER, useFactory: () => () => {} });
// Type error. APP_INITIALIZER should be an array of functions, but the return type of the factory is a single function.
// Type '() => void' is not assignable to type 'readonly (() => void | Observable<unknown> | Promise<unknown>)[]'
provideMulti({ token: TitleStrategy, useClass: AppTitleStrategy });
// Type error. TitleStrategy does not support multi providing.
// Argument of type '{ token: typeof TitleStrategy; useClass: typeof AppTitleStrategy; }' is not assignable to parameter of type 'never'.

provideMulti({ token: APP_INITIALIZER, useFactory: () => () => {} });
// Legit. The return type of the factory matches the item type of APP_INITIALIZER

Implementation

export function provide<T, U extends T>(config: ProvideConfig<T, U>): Provider {
  if ('useValue' in config)
    return { provide: config.token, useValue: config.useValue };
  if ('useFactory' in config)
    return { provide: config.token, useFactory: config.useFactory };
  if ('useClass' in config)
    return { provide: config.token, useClass: config.useClass };
  if ('useExisting' in config)
    return { provide: config.token, useExisting: config.useExisting };
  throw new Error('Invalid config'); // impossible to happen
}

export function provideMulti<T>(config: ProvideMultiConfig<T>): Provider {
  return {
    ...provide(config as ProvideConfig<any>),
    multi: true,
  } as Provider;
}
export type ProvideConfig<T, U extends T = T> = ProvideConfigToken<T> &
  ProvideConfigUse<U>;

export type ProvideMultiConfig<T> = ProvideConfigToken<T> &
  (T extends readonly (infer I)[] ? ProvideConfigUse<I> : never);

export interface ProvideConfigToken<T> {
  token: ProviderToken<T>;
}

export type ProvideConfigUse<T> =
  | ProvideConfigUseValue<T>
  | ProvideConfigUseFactory<T>
  | ProvideConfigUseClass<T>
  | ProvideConfigUseExisting<T>;
export interface ProvideConfigUseValue<T> {
  useValue: T;
}
export interface ProvideConfigUseFactory<T> {
  useFactory: () => T;
}
export interface ProvideConfigUseClass<T> {
  useClass: Type<T>;
}
export interface ProvideConfigUseExisting<T> {
  useExisting: ProviderToken<T>;
}

Alternatives considered

N/A

@JeanMeche
Copy link
Member

Hi,
We related issues on that topic we #28778 and #51675. Right now, InjectionToken does not provide info on if the token should be multi or not.

@Char2sGu
Copy link
Author

I see. I think it would still be nice to add provide().

@alxhub
Copy link
Member

alxhub commented Apr 26, 2024

This has been on my mind too, as a potential way to add incremental type safety to DI.

@alxhub alxhub added feature Issue that requests a new feature area: core Issues related to the framework runtime core: di cross-cutting: types labels Apr 26, 2024
@ngbot ngbot bot added this to the Backlog milestone Apr 26, 2024
@alxhub
Copy link
Member

alxhub commented Apr 26, 2024

Also, 👏 nice issue number!

@Char2sGu
Copy link
Author

lol I didn't notice the number.

@Char2sGu
Copy link
Author

Do you expect me to create a PR? What am I supposed to do next?

@SeregPie
Copy link

very similar to my proposal.

#55054

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: di cross-cutting: types feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests

4 participants