sveltejs / rfcs

RFCs for changes to Svelte
275 stars 82 forks source link

TypeScript: Explicitly Typing Props/Slots/Events + Generics #38

Open dummdidumm opened 4 years ago

dummdidumm commented 4 years ago

rendered

numfin commented 4 years ago

Why not use 1 interface for props/events/slots ? This way we can create events, that depends on props:

interface ComponentDefinition<T extends Record<string, string>> {
  props: { a: T },
  events: { b: T }
}

p.s. Sorry for bad english, tried my best.

dummdidumm commented 4 years ago

Yes this would be possible through the ComponentDef interface

stefan-wullems commented 4 years ago

For the slots, props and events, I would lose the Component prefix:

Then finally ComponentDef -> Component.

Doesn't make the names a lot more ambiguous or prone to conflict with existing types I think.

stefan-wullems commented 4 years ago

Perhaps separating the type definition from the logic of a component would work nicely.

<script lang="ts" definition>
  interface Component<T> {
     props: { value: T }
     events: { 
       change: (value: T) => void
     }
     slots: {
       default: {}
     }
  }
</script>

<script lang="ts">
  export let value
</script>

<some>
  <slot>Markup</slot>
</some>
dummdidumm commented 4 years ago

I like the shortening of the names although I think this might increase the possibility of name collisions. I'm against putting this into a separate script. This would require more effort in preprocessing, also I like the colocation of the interfaces within the instance script.

AlexGalays commented 4 years ago

Can't wait to use this <3

🙏

dummdidumm commented 4 years ago

Could you elaborate on your second point a little more? I'm don't fully understand the use case. And what do you mean by "impossible"? Possible to do in Svelte but the type checker complains?

AlexGalays commented 4 years ago

(unless I missed something)

you can't pass a component instance to a slot so people end up either

but today, we have no way to express that Props should be SomeSvelteComponent's Props beside triple checking manually.

francoislg commented 4 years ago

Stumbled upon this and just wanted to throw here a slight variation of Option 2:

<script lang="ts">
    import {createEventDispatcher} from "svelte";

    type T = ComponentGeneric<boolean>; // extends boolean
    type X = ComponentGeneric; // any

    export let array1: T[];
    export let item1: T;
    export let array2: X[];
    const dispatch = createEventDispatcher<{arrayItemClick: X}>();
</script>

I think it would be slightly closer to TypeScript code than a ComponentGenerics interface that gets magically expanded :smile:

denis-mludek commented 3 years ago

Hi !

Any idea when Generic Component will be available/released? Is it perhaps a case of choice paralysis? I think we would all love something even if it's not 100% perfect!!

I'm working on a SvelteTS project for several weeks now, and I would have used this feature a few times already. Btw, love the work you're doing to make SvelteTS a thing. TS support was the thing that made me switch from React to Svelte. 🤗

dummdidumm commented 3 years ago

@tomblachut tagging you since you are the maintainer of the IntelliJ Svelte Plugin - anything in that proposal that concerns you implementation-wise ("not possible to implement on our end")?

tomblachut commented 3 years ago

@dummdidumm thank you for tagging me.

Generics will definitely be, as you've written, an uncanny valley and maintenance burden.

Option 1 & 2 without Svelte support in editor will produce invalid TS, given that both will require special reference resolution. I think it's better to avoid that.

I'd scratch Option 2, because ComponentGenerics is in the same scope as things that will refer to its type parameters. I imagine it will add some implementation complexity AND mental overhead for users.

I quite like Option 3 because it's valid TS. ComponentGeneric would be treated as identity mapped type.

    type T = ComponentGeneric<boolean>; // extends boolean
    type X = ComponentGeneric; // any

Option 3 could be even simplified a bit by giving new semantics to export type in similar way as export let denotes a prop

    export type T = boolean;
    export type X = any;

Now, I think it's better to stick to one style of declarations: (separate interfaces/compound ComponentDef/namespace) otherwise we may introduce small bugs in one of them and more importantly will need to decide on and teach about precedence.

One additional thing this proposal does not mention is ability to extend interfaces. I think that's great feature. Author of the component could say "this "PromotedPost adheres to Post props" and whenever types are changed in Post definition, implementing components would show type errors. Unless I'm missing something interfaces will support that use case out of the box.

dummdidumm commented 3 years ago

Thanks for your insights!

I agree that we should only provide one style of declarations. Separate interfaces feels like the best option there.

I also agree that for generics option 3 feels the closest to vanilla TypeScript which is why I prefer that, too. That simplification does not feel quite right for me though, because we are not exporting that generic to anyone, we are just stating that the component is generic. Option 3 has one little shortcoming though, being not being strict enough. Take this code snippet:

type T = ComponentGeneric<{a: boolean}>;
const t: T = {a: true};

Without extra Svelte-specific typing-work, this snippet would not error, because TS does not think of T as a generic. If it did, it would error with Type '{ a: true; }' is not assignable to type 'T'. '{ a: true; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ a: boolean; }'. In general, errors related to T would not be in the context of generics. I'd say this is a hit we can take though, because the types are "good enough" for most use cases, and with some extra transformation work (like svelte2tsx) you can even make TS think that this is a generic - and also it's just not possible to make TS think of T as a generic at the top level without transformations.

One thing you brought up about extending interfaces is a very good advantage, but it also got me thinking how to deal with generics in that context.

For example you have this interface:

export interface ListProps<ListElement> {
   list: ListElement[];
}

How do you extend it while keeping it generic in the context of a Svelte component? The only possibility that comes to my mind is to do this:

<script lang="ts">
  import type { ListProps } from '..';

  type BooleanListElement = ComponentGeneric<boolean>;
  interface ComponentProps extends ListProps<BooleanListElement> {}

  export let list: BooleanListElement[];
</script>
..
non25 commented 3 years ago

I miss generics too. I'll leave one example which I would really appreciate to use generics with:

This checkbox selector returns subset of an array of objects without mutating them, which is really handy, but it will return any[] instead of passing types forward, which is sadge... :disappointed:

<script>
  import Checkbox from '../components/Checkbox.svelte';

  export let checkboxes = [];
  export let checked = [];
  export let idF = 'id';
  export let textF = 'text';

  let checkedIds = new Set(checked.map((c) => c[idF]));

  function mark(id) {
    checkedIds = new Set(
      checkedIds.has(id)
        ? [...checkedIds].filter((cid) => cid !== id)
        : [...checkedIds, id]
    );
  }

  $: checked = checkboxes.filter((c) => checkedIds.has(c[idF]));
</script>

<ul class="checkboxes">
  {#each checkboxes as checkbox (checkbox[idF])}
    <li>
      <Checkbox
        checked={checkedIds.has(checkbox[idF])}
        on:change={() => mark(checkbox[idF])}
        desc={checkbox[textF]}
      />
    </li>
  {/each}
</ul>

This is proposed way to use generics? I don't think I understood examples correctly, so I added type definitions to this component.

<script lang="ts">
  import Checkbox from '../components/Checkbox.svelte';

  type Item = ComponentGeneric; // how this will attach to the checkboxes prop ?

  export let checkboxes: Item[] = [];
  export let checked: Item[] = [];
  export let idF = 'id';
  export let textF = 'text';

  let checkedIds = new Set<number>(checked.map((c: Item) => c[idF]));

  function mark(id: number) {
    checkedIds = new Set(
      checkedIds.has(id)
        ? [...checkedIds].filter((cid: number) => cid !== id)
        : [...checkedIds, id]
    ):
  }

  $: checked = checkboxes.filter((c: Item) => checkedIds.has(c[idF]));
</script>
dummdidumm commented 3 years ago

Everything inside this proposal is type-only, which means it's only there to assist you at compile time to find errors early - nothing of this proposal will be usable at runtime, similar to how all TypeScript types will be gone at runtime.

In your example you would do:

<script lang="ts">
  // ..
  type Item = ComponentGeneric;
  type ItemKey = ComponentGeneric<keyof Item>;

  export let checkboxes: Item[] = [];
  export let checked: Item[] = [];
  // Note: For the following props, the default value is left out because you cannot expect "id" or "text" to be present as a default if you don't narrow the Item type
  export let idF: ItemKey;
  export let textF: ItemKey;
// ..
non25 commented 3 years ago

Wow, keyof is cool.

I want to make sure that I understand correctly, so here's another example:

<script lang="ts">
  interface Vegetables {
    id: number;
    name: string;
    weight: number;
  }

  let someItems: Vegetables[] = [
    ...
  ];

  let checked: Vegetables[] = [];
</script>

<CheckboxSelector checkboxes={someItems} textF="name" idF="id" bind:checked />
<!--                            ^                ^ 
                                |                | Could it attach to this?
                                |
                                | how do I specify that ComponentGeneric
                                  should attach to this?
-->

For example here's how I would use this in regular typescript, which is clear to me.

function component<T>(checkboxes: T[] = [], idF: <keyof T>, textF: <keyof T>) {
  ...
}
dummdidumm commented 3 years ago

When using the component, you don't specify anything, you just use the component and the types should be inferred and errors should be thrown if the relationship between is incorrect. So in your example if you do textF="nope" it would error that nope is not a key of Vegetables.

Doing

type T = ComponentGeneric;
export let checked: T[];
export let idF: keyof T;
// ...

Would be in the context of Svelte components semantically the same as

function component<T>(checked: T[], idF: keyof T) {
  ...
}

This is the uncanny valley I'm talking about in the RFC which I fear is unavoidable - it doesn't feel the same like generics, yet it serves this purpose inside Svelte components (and inside only Svelte components). The problem is that Svelte's nice syntax of just directly starting with the implementation without needing something like a wrapping function, so there is no place to nicely attach generics.

non25 commented 3 years ago

So if I end up in a situation where I need two generics:

function component<X, Y>(items: X[], someOtherProp: Y) {
  ...
}

How that would look in the proposed approach? :thinking: Is it even possible ?

Do you know how bindings will work in the current state?

In the example above I added annotation that checked in parent component is of same type as someItems, but if we pass someItems through checkboxes prop, we should lose the type, because we can only use any as 'generic' type now in the CheckboxSelector component, right ?

Do annotations to a bind override any[] in this case ? :thinking:

Sorry for the wording...

dummdidumm commented 3 years ago

So if I end up in a situation where I need two generics:

function component<X, Y>(items: X[], someOtherProp: Y) {
  ...
}

How that would look in the proposed approach? 🤔 Is it even possible ?

You do this

type X = ComponentGeneric;
type Y = ComponentGeneric;
export let items: X[];
export let someOtherProp: Y;

Do you know how bindings will work in the current state?

In the example above I added annotation that checked in parent component is of same type as someItems, but if we pass someItems through checkboxes prop, we should lose the type, because we can only use any as 'generic' type now in the CheckboxSelector component, right ?

Do annotations to a bind override any[] in this case ? 🤔

Sorry for the wording...

Sorry, I don't know what you mean.

non25 commented 3 years ago

Sorry, I don't know what you mean.

I hope this is a better way to explain. :thinking:

<script lang="ts">
  interface Vegetables {
    id: number;
    name: string;
    weight: number;
  }

  let someItems: Vegetables[] = [
    ...
  ];

  // I annotate same type to the checked prop, which I will bind below
  // What type checked will have after the bind ?
  let checked: Vegetables[] = [];
  // I need to use checked in other places and want it to retain the type
</script>

<CheckboxSelector checkboxes={someItems} textF="name" idF="id" bind:checked />
<!--                                                                  ^
                                                          binding checked here
-->

<script lang="ts">
  // ...
  // I set any here because I want to accept any object type
  // and generics is currently not supported
  export let checkboxes: any[] = [];

  // this prop will contain a subset of checkboxes prop,
  // which I bind above
  export let checked: any[] = [];
  // ...
</script>
dummdidumm commented 3 years ago

To me this sounds like you mix up some of TypeScript's type system with regular JavaScript. For example let checked = Vegetables[] is not valid TypeScript, because Vegetables is an interface, which does not exist at runtime. It should be let checked: Vegetable[], which also answers your second question: The type will stay Vegetable because you told TypeScript that it's of this type, that does not change. Instead, TypeScript would a compiler error if you would try to assign something to checked that does not suffice the Vegetable interface (any is okay, because you can assign any to anything).

non25 commented 3 years ago

To me this sounds like you mix up some of TypeScript's type system with regular JavaScript.

I'm just making mistakes, not used to type let something: type[] = []; :grin: Thanks for the explanation. That's better than nothing.

So looks like the most negatively affected use cases without generics are:

?

tomblachut commented 3 years ago

Generics come in handy if you want connect types of 2 or more things to each other. Fo example you may want to guarantee that some event's payload will be of the same type as a prop, or some derivative of that type.

tomblachut commented 3 years ago

@dummdidumm

Option 3 has one little shortcoming though, being not being strict enough.

Ouch, TIL. Thanks for catching that. Actually not "TIL", I spend couple of days pondering about that 😅 I agree that it's a hit we can take since this problem has additive solution. We can wire up annotations for this on top of normal TS, in contrast to suppressing some false-negatives. 👍

export has one advantage over ComponentGeneric - it's not a reference. Though, if it's okay to enable import { ComponentGeneric } from 'svelte'; I'm okay with both approaches. I'd just prefer to avoid global implicit type.


<script lang="ts">
  import type { ListProps } from '..';

  type BooleanListElement = ComponentGeneric<boolean>;
  interface ComponentProps extends ListProps<BooleanListElement> {}

  export let list: BooleanListElement[];
</script>

I think your example is elegant.

I didn't consider yet how extends will integrate with SvelteComponentTyped. Those 3 interfaces would need to be translated into generics of that class. Will it even work?

dummdidumm commented 3 years ago

Implicit global type - I'm a little split on this. There are some other globals already after all, on the other side it's more explicit. Either way, its presence would need extra tooling code (that "it's not the same in TS" thing I mentioned earlier).

About adding it to SvelteComponentTyped: Do you mean from a tooling perspective how to automatically generate type definitions for libraries, or how to generate the code for the IDE? Either way, some additional logic would be necessary to first extract the interface out to the top level so they can be used on the class, and then another step where the ComponentGeneric usages are collected and added in order to SvelteComponentTyped like export class MyComponent<T, X extends string> extends SvelteComponentTyped<{ prop: T },... Interfaces could be extracted the same, so that

type T = ComponentGeneric;
interface ComponentEvents {
  foo: CustomEvent<T>
}

Becomes

interface ComponentEvents<T> ...

export class MyComponent<T> extends <.. ComponentEvents<T>..
pitaj commented 3 years ago

I understand wanting to use as little extra grammar as possible, but I think we should consider using export to signify a generic type and for specifying the interface of props, slots, and events.

This has a few benefits:

<script lang="ts">
    import {createEventDispatcher} from "svelte";

    // required generic type extending boolean
    export type T extends boolean;
    // optional generic type
    // defaults to string
    export type X = string;

    // instead of ComponentSlots
    export interface Slots {
        default: { aSlot: T }
    }

    export let array1: T[];
    export let item1: T;
    export let array2: X[];
    const dispatch = createEventDispatcher<{arrayItemClick: X}>();
</script>

The only thing in the example that isn't valid typescript syntax is export type T extends boolean, which IMO is not asking for much.

dummdidumm commented 3 years ago

Adding invalid TS syntax is a no-go for me, it would require every IDE to know of this and people need to get used to that for Svelte only. Moreover, there wouldn't be things like optional generics. Generics need to be driven by the prop input. About the exports: I'm hesitant because exports are right now reserved for props. On the module level, you can also export interfaces/types, but the meaning is different. I'm not sure if it would bring more confusion than helping. On the other hand one could argue that it does align with the "public API" semantics of export let.

If we go with ComponentGeneric I think I have one more argument in favor of implicit global: If it's an explicit import, people might think they can model their API like this outside of Svelte components, which is wrong. With a global type, they would see that TS will throw an error ("unknown type") if you try that outside.

Monkatraz commented 3 years ago

Just throwing stuff at the wall...

<script lang="ts">
  import {createEventDispatcher} from "svelte";

  // this is wacky as all hell but kinda sorta less globals (syntax is more rigid??)
  type T<V extends boolean = Generic> = V
  type X<V = Generic> = string

  export let array1: T[];
  export let item1: T;
  export let array2: X[];
  const dispatch = createEventDispatcher<{arrayItemClick: X}>();
</script>

Honestly, this is the only alternative I could come up with. I think ComponentGeneric is probably the way to go unless anyone gets any ideas or an epiphany. Maybe my attempt will inspire someone, lol.

EDIT: Hm, actually, this should work too:

<script lang="ts">
  import {createEventDispatcher} from "svelte";

  export type T<V extends boolean = Generic> = V
  export type X = string

  export let array1: T[];
  export let item1: T;
  export let array2: X[];
  const dispatch = createEventDispatcher<{arrayItemClick: X}>();
</script>

It's a bit nicer if you have default values, but I still don't think it's good enough. The noise coming from declaring a type parameter is pretty high.

also I prefer the export type solution in general as it has a lot less noise dont at me

Monkatraz commented 3 years ago

Also, wouldn't using $$ prefixes instead of Component match Svelte patterns better?

<script lang="ts">
    import {createEventDispatcher} from "svelte";

    type T = $$Generic<boolean>; // extends boolean
    type X = $$Generic; // any

    // you can use generics inside the other interfaces
    interface $$Slots {
        default: { aSlot: T }
    }

    export let array1: T[];
    export let item1: T;
    export let array2: X[];
    const dispatch = createEventDispatcher<{arrayItemClick: X}>();
</script>
pitaj commented 3 years ago

Adding invalid TS syntax is a no-go for me, it would require every IDE to know of this and people need to get used to that for Svelte only. Moreover, there wouldn't be things like optional generics. Generics need to be driven by the prop input.

Can you explain more why optional generics shouldn't be a thing? I'm not quite grasping your reasoning.

But if there aren't optional types, you can just use the right hand side as the base type.

export type T = string; // generic type T extends string

I'm hesitant because exports are right now reserved for props. On the module level, you can also export interfaces/types, but the meaning is different. I'm not sure if it would bring more confusion than helping. On the other hand one could argue that it does align with the "public API" semantics of export let.

They're modeled explicitly after the way props work. I think following that model would make following this more intuitive and more discoverable. For instance, setting a generic could be just like using a prop:

// Foo.svelte
<script lang="ts">
export type X = string;
</script>

// Bar.svelte
<script lang="ts">
import Foo from './foo';

interface Stuff {}
</script>

<Foo X={Stuff} />

I don't think setting them explicitly is planned as part of this rfc it's just an example to demonstrate my line of thinking.

Before I knew this wasn't possible yet, I tried doing stuff similar to this (to be met with errors of course).

tomblachut commented 3 years ago

@pitaj Sorry, but we can't make following happen:

<Foo X={Stuff} />

You can already create an uppercase prop (https://svelte.dev/repl/2897e47e9abf4858adc46978db9a1717?version=3.33.0), passing generics explicitly would require different syntax. This proposal doesn't cover that, nor should it. Let's agree on first part that is generics declaration + possibility to infer them from props/events/slots passed to the component.

export type T extends boolean;

Strong no from me. This is invalid TS, we can't invent new syntax, in addition to @dummdidumm points, what if TS decides to add extends X part in future version with different meaning that ours.


If we go with ComponentGeneric I think I have one more argument in favor of implicit global: If it's an explicit import, people might think they can model their API like this outside of Svelte components, which is wrong. With a global type, they would see that TS will throw an error ("unknown type") if you try that outside.

That's a fair point. I still prefer export but you're dragging me to your side step by step. I thought a bit more about implicit global implementation and it should be easier than I initially anticipated. This would be slightly parallel to $$props so maybe @Monkatraz $$Generic is a better fit.

pitaj commented 3 years ago

Sorry, but we can't make following happen

I wasn't suggesting it, I was just demonstrating how my thought process went while initially looking for this feature.

Strong no from me. This is invalid TS, we can't invent new syntax

That's fair, alternatives have been proposed.

dummdidumm commented 3 years ago

I agree with the $$-syntax, it chimes well with the Svelte "language". Updates the rfc accordingly.

We did discuss this in the maintainer's meeting and I got positive feedback on the current state. One thing that came up is the possibility for the ability to export the interfaces for usage in other components to extend it there. Got to think about how that might work in combination with generics (maybe it won't).

pitaj commented 3 years ago

Is it a goal of this RFC to propose a way to explicitly designate generic types, or would you rather require all generics be derived from the given props?

For explicitly designating types, would it not be possible to add a new syntax similar to a prop modifier generic:<GenericTypeName>(={<Type>}) that could be used like so:

<Component generic:X={string} />

Or like so with a shorthand

<script lang="ts">
interface Item {
  name: string;
  id: number;
}
</script>

<Component generic:Item /> <!-- `Component` has a generic named `Item` -->

Perhaps one can only explicitly designate the generic type if it is exported.

dummdidumm commented 3 years ago

Explicitly declaring the generic is out of scope of this rfc, and at the moment I'm not even sure something like this is needed, since I don't see how generics can be driven by something else than props - and if you pass in these props, the generic type should be inferred from that.

pitaj commented 3 years ago

I think one is where the prop types are derived from the generic:

types.d.ts

interface A<T> { ... }
interface B<T> { ... }

Component.svelte

<script type="ts">
type X = $$Generic;

export let a: A<X>;
export let b: B<X>;
</script>

Other.svelte

<script type="ts">
import Component from './Component.svelte'

type Union = string | number;

let a: A<Union> = 'Hello, world';
let b: B<Union> = 123;
</script>

<Component a b />

Inference probably could figure out that X == Union, but does this hold in any case, no matter the complexity?

gustavopch commented 3 years ago

I know explicitly passing type parameters is out of the scope, but I'd just like to point that it's an already resolved thing in JSX, so if it's ever needed for Svelte in the future, JSX can be followed:

<Formik<FormValues>
  initialValues={initialValues}
  validateOnBlur={false}
  validate={onValidate}
  onSubmit={onSubmit}
>
dummdidumm commented 3 years ago

The Svelte compiler would need to be adjusted then, which would be the first time that would need to be done for a typescript feature.

AlexGalays commented 3 years ago

I know explicitly passing type parameters is out of the scope, but I'd just like to point that it's an already resolved thing in JSX, so if it's ever needed for Svelte in the future, JSX can be followed:

<Formik<FormValues>
  initialValues={initialValues}
  validateOnBlur={false}
  validate={onValidate}
  onSubmit={onSubmit}
>

That doesn't do a whole lot though? Beside more precise compilation errors perhaps. Nowadays TS is pretty good at inferring so as long as each individual props inside the component are well typed relative to each other, it seems redundant to explicitely set the type on the call site? Anyway, it's not a must-have feature like the one discussed here!

gustavopch commented 3 years ago

@AlexGalays I'm not using Svelte in my projects yet, so I can't really say how useful explicit type parameters would be. What I do know is that in JSX they are very useful for example in the following snippet:

<Formik<FormValues>
  initialValues={{
    name: ''
  }}
/>

Without an explicit type parameter, initialValues would accept anything and try to infer the type from that. But what if I already have a type and want to make sure initialValues conforms to it? Then I have to either (a) declare const initialValues: FormValues = { ... } somewhere outside of the JSX or (b) pass FormValues as an explicit type parameter and keep initialValues inlined.

Anyway, I'm not saying that it should be implemented, I'm just throwing some information for someone reading this in the future in case it's ever decided explicit type parameters are needed.

dummdidumm commented 3 years ago

Experimental support for typing props/events/slots and generics is now available as described in this RFC. Please provide feedback in the following issue: https://github.com/sveltejs/language-tools/issues/442 The feature is experimental and subject to change. It therefore doesn't follow semantic versioning yet.

dummdidumm commented 2 years ago

https://github.com/sveltejs/language-tools/issues/1326 proposed to add style props (<Component --style-prop="value" />) to this, too. It also brought up the good question about "what do JS users do?" because they can't use interfaces, they have to rely on JSDoc. We should probably enhance the RFC and find a JSDoc-compatible version, too. I think we can take inspiration for the API from the great work of sveld (kudos to @metonym).

Note to self: If we add support for generics via JSDoc, we need to adjust the transformation because right now it relies on using TS syntax in the transform output (since we know it's TypeScript). Not sure if we can use the same transformation then.

VdustR commented 2 years ago

Thank you for the awesome work!

Is there example for generic parameter defaults? I can't find that in the RFC.

function foo<T extends Item = string>(..)

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults

dummdidumm commented 2 years ago

What's the use case for specifying defaults? Could you give an example where this is helpful/needed?

VdustR commented 2 years ago

I made a function with generic parameter defaults like this:

export const defaultGen = () => Math.random().toString(16).substring(2);

function Likftc<
  Source extends number | string,
  Target extends number | string = string
>(
  initialKeys: Source[] = [],
  generator: () => Target = defaultGen as () => Target
)

Full code: https://github.com/VdustR/likftc/blob/f0203a9d3e71a9513d396f4809369778a3dfbc25/packages/core/index.ts#L3

The function type will be like:

let target = "foo";
Likftc([1, 2, 3], () => target).get(1) // get: (item: 1 | 2 | 3) => string

let target = 1234;
Likftc([1, 2, 3], () => target).get(1) // get: (item: 1 | 2 | 3) => number

Likftc([1, 2, 3]).get(1) // get: (item: 1 | 2 | 3) => string

// without generic parameter defaults
Likftc([1, 2, 3]).get(); // get: (item: 1 | 2 | 3) => string | number

With the generic parameter defaults I can use the default type if the argument is not passed.

I also make a react hook depending on it like this:

function useLikftc<
  Source extends number | string,
  Target extends number | string = string
>(frame: Source[], generator?: () => Target)

Full code: https://github.com/VdustR/likftc/blob/f0203a9d3e71a9513d396f4809369778a3dfbc25/packages/react/index.ts#L4

And it will work like:

let target = 123;
useLikftc([1, 2, 3], () => target).get(); // get: (item: 1 | 2 | 3) => number

let target = "foo";
useLikftc([1, 2, 3], () => target).get(); // get: (item: 1 | 2 | 3) => string

useLikftc([1, 2, 3]).get(); // get: (item: 1 | 2 | 3) => string

// without generic parameter defaults
useLikftc([1, 2, 3]).get(); // get: (item: 1 | 2 | 3) => string | number

I use component with let: directive as the react custom hook alternative in svelte (reference: https://gradientdescent.de/custom-hooks/#Mypreferredsolution). The svelte component props are like this:

type Source = $$Generic<number | string>;
type Target = $$Generic<number | string>; // here is the generic parameter defaults needed
export let keys: Source[];
export let generator: (() => Target) | undefined = undefined;

Full code: https://github.com/VdustR/likftc/blob/f0203a9d3e71a9513d396f4809369778a3dfbc25/packages/svelte/index.svelte#L8

The component would be used like:

<Likftc keys={[1, 2, 3]} generator={() => 'foo'} let:get />  <!-- get: (item: number) => string -->
<Likftc keys={[1, 2, 3]} generator={() => 'foo'} let:get />  <!-- get: (item: number) => number -->

<Likftc keys={[1, 2, 3]} let:get />  <!-- get: (item: number) => string | number -->
         <!-- It's better to be typed as `get: (item: number) => string` in this case  -->

With the generic parameter defaults, the component could be defined with the default type if the property is not passed. For now let:get will return with type get: (item: number) => string | number if I don't pass generator and it's better to be typed as get: (item: number) => string.

icalvin102 commented 2 years ago

I just stumble across this the other day and I really like it. Thanks for the awesome work.

One thing I noticed though: If I try to override a event that is already predefined by svelte it wont give the correct type.

const dispatch = createEventDispatcher<{'input': Foo, 'customInput': Foo }>();

Produces the following types:

on:input // CustomEvent<any>
on:customInput // CustomEvent<Foo>

This is true for all predefined Events that I tested (submit, change, input ...)

The ability to override would be very nice IMO.

hperrin commented 2 years ago

Is there a way to define defaults on generics? In TS, you can do T extends SomeInterface = SomeClass, and it will know, if you didn't supply T, that T is the SomeClass type. Can we do the same with this solution?

hperrin commented 2 years ago

I also seem to be having trouble returning a generic from a function. No matter what you give the generic, it always types the return value in the widest way possible. Here's an example:

 <!-- Example.svelte -->
<svelte:element this={tag} bind:this={element} {...$$restProps}
  ><slot /></svelte:element
>

<script lang="ts">
  type TagName = $$Generic<'input' | 'span'>;
  interface $$Props
    extends svelte.JSX.HTMLAttributes<HTMLElementTagNameMap[TagName]> {
    tag: TagName;
  }

  export let tag: TagName;

  let element: HTMLElementTagNameMap[TagName];

  export function getElement() {
    return element;
  }
</script>
<!-- UseExample.svelte -->
<Example tag="input" bind:this={example} />

<script lang="ts">
  import { onMount } from 'svelte';
  import Example from './Example.svelte';

  let example: Example<'input'>;

  onMount(() => {
    const el = example.getElement();
  });
</script>

When you hover over el, you get const el: HTMLInputElement | HTMLSpanElement as the type, and trying to use el.value results in:

Property 'value' does not exist on type 'HTMLInputElement | HTMLSpanElement'.
  Property 'value' does not exist on type 'HTMLSpanElement'.ts(2339)

Am I correctly following the RFC in this example? Is this an expected error?

AlbertMarashi commented 2 years ago

I'm getting a bug when nesting multiple generic types within components

Main.svelte

<script>
import Foo from "./Foo.svelte"

</script>
<Foo xyz="foo" >
    <div slot="option" let:xyz>
        <h1>{xyz}</h1>
    </div>
</Foo>

The type of xyz here is unknown when it should be string

Foo.svelte

<script lang="ts">
import Bar from "./Bar.svelte"

type T = $$Generic
export let xyz: T

</script>
<Bar {xyz}>
    <svelte:fragment slot="option" let:bar>
        <slot name="option" xyz={bar}/>
    </svelte:fragment>
</Bar>

Bar.svelte

<script lang="ts">
type T = $$Generic
export let xyz: T

</script>
<slot name="option" bar={xyz} />
Tal500 commented 2 years ago

I've tried the generic feature of components, really helpful! Suggestions:

  1. Like it was told at least twice here, I think it should support default generic arguments. My use case is when you want to get the component as a prop of a different component (when you want to save the result of the component via bind:this, or when you want to pass a component constructor to a different component). Yes, I could say explicitly what I want there, but why? Having a default value also suggests the user (that use a generic component library, say) what the 'default-behavior' or the 'unspecified' value should be.
  2. We can allow typing on JS (including the generics), if we will use JSDoc to embed TS-defs. For example, we can define the generics in JS via @typedef.