Repository: angular
Issue: [Feature] Introduce Type-safe Token Providing
Labels: feature,area: core,core: di,cross-cutting: types

### 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 `ProviderToken`s 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

```ts
function provide<T, U extends T>(config: ProvideConfig<T, U>): Provider;
function provideMulti<T>(config: ProvideMultiConfig<T>): Provider;
```

## Usage

```ts
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>)[]'
```

```ts
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

```ts
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;
}
```

```ts
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