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

[Bug]: Angular & Typescript: Update StoryObj to handle new InputSignal #25784

Closed
valentinpalkovic opened this issue Jan 28, 2024 Discussed in #25781 · 1 comment · Fixed by #26413
Closed

[Bug]: Angular & Typescript: Update StoryObj to handle new InputSignal #25784

valentinpalkovic opened this issue Jan 28, 2024 Discussed in #25781 · 1 comment · Fixed by #26413

Comments

@valentinpalkovic
Copy link
Contributor

Discussed in #25781

Originally posted by boblelot January 28, 2024

Is your feature request related to a problem? Please describe.

Angular has added a new developer-preview feature for inputs, the new way allows for easier reactive code, and follows the Signal pattern.
syntax for a single member input is class MyComponentWithInput{ myInput = input<string>("value") }

https://angular.io/api/core/InputSignal

Since the new way changes the class field type from number to InputSignal<number> , StoryObj expects us to provide args of type InputSignal but angular wants a string

Given a component like

import { Component, Input, InputSignal, computed, input } from '@angular/core';

function coerceBoolean(val: any): boolean {
  return !!val
}
@Component({ ... })
export class MyTestComponent {
  name = input<string>("name"); // new way for firstName
  @Input() lastName: string = "lastName";  // old way for lastName, we can mix and match old and new
  // the second accepted generic type defines the input type for the transformer function
  suspended = input<boolean, string>(true, {
    transform: coerceBoolean
  });
   /** Input with its full typing */
  counter: InputSignal<number> = input(5);
   /** This field must be set by it's alias when value is provided */
  fieldWithAlias = input<string>("my alias is foo", {
    alias: "foo"
  });
  // Reactive computed value
  readonly greeting = computed(() => 'Hello ' + this.name() + " " + this.lastName);
}

Id like type Story = StoryObj<MyTestComponent>; to adapt the component signal inputs to be their generic type

type adaptedComponent = {
    name: string;
    lastName: string;
    suspended: string;
    counter: number;
    fieldWithAlias: string;
    readonly greeting: Signal<string>;
}

Bonus request: is there a way to also handle the alias with Typescript types?

Describe the solution you'd like

I've had some success with a adating the component before passing it to StoryObj<>
The type SignalInputCmp<> accepts any object and maps over each fields of type InputSignal to inferred their generic type.

// Extract the generic type from InputSignal
type InferInputSignalType<P> = P extends InputSignal<infer T> ? 
    T :  // is Signal, return the first generic type
    P extends InputSignal<infer T, infer C> ? C : // Signal has transformer, return the second generic type
    never;  
// Go over every member and convert any InputSignal field to their accepted generic.
type SignalInputCmp<T extends Record<string, any>> = {
  [key in keyof T]: T[key] extends InputSignal<any> ?
    InferInputSignalType<T[key]> :  // infer type
    T[key] // is not a Signal, return raw type
}

Used like

export default meta;
type adaptedComponent = SignalInputCmp<MyTestComponent>;
type Story = StoryObj<adaptedComponent>;

export const Primary: Story = {
  args: {
    name: "salt",
    lastName: "pepper",
    suspended: "false",
    fieldWithAlias: "this field name is not used, we must use the alias name 'foo'"
  },
};


export const Alt: Story = {
  args: {
    foo: "alias is respected but type Story still uses field name 'fieldWithAlias'"
  } as any, // have to cast to `any` so i can use the alias `foo`
};

Describe alternatives you've considered

No response

Are you able to assist to bring the feature to reality?

no

Additional context

The provided code examples work with Angular 17.1.1

It's missing support for two of the Angular input features.

  • required: input.required<number>();
  • alias: input(0,{alias:"foo"})

https://angular.io/api/core/Input

@Johanneslueke
Copy link

FYI - i tried your workaround. You mentioned this does not work for "input.required" and "input alias" . I am able to report that your solution does work with angular 17.2.

i only had to adjust your solution a tiny bit:

 
type InferInputSignalType<P> = P extends InputSignal<infer T> ? 
    T : 
    P extends InputSignalWithTransform<infer T, infer C> ? C : // This line changed !!!
    never;  

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment