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

Supports union types in props and reactive statement #214

Closed
jasonlyu123 opened this issue Jun 21, 2020 · 28 comments
Closed

Supports union types in props and reactive statement #214

jasonlyu123 opened this issue Jun 21, 2020 · 28 comments
Labels
feature request New feature or request

Comments

@jasonlyu123
Copy link
Member

jasonlyu123 commented Jun 21, 2020

Is your feature request related to a problem? Please describe.
The following code will have a typescript error due to typescript's control flow analysis.
But this is very useful when it comes to a select-wrapper component.

<script lang="typescript">
  export let value: string | number = '';

  if (typeof value === 'number') {
    console.log(value.toFixed())
  }

</script>

<select bind:value={value}></select>

Describe the solution you'd like
In the svelte2tsx, the props and reactive statement $: { } needs to be taken out of the control flow context maybe by wrapping it with a callback like this:

declare function __sveltets_invalidate<T>(getValue: () => T): T

/*export*/ let value: string | number = __sveltets_invalidate(() => '')

let b: 'A' | 'B' = 'A';

() => {
$: {
    console.log(b)
}
}

Additional context
Add any other context or screenshots about the feature request here.
圖片

@dummdidumm
Copy link
Member

Good idea with the invalidate-wrapper around props. Maybe we don't even need to add another wrapper to reactive statements then? Or are there any cases where this will throw control flow errors?

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 22, 2020

change the export let to let and wrap the statement in $: {} will have the same error

<script lang="typescript">
  let value: string | number = '';

  $: {
    if (typeof value === 'number') {
      console.log(value.toFixed())
    }
  }
</script>

if value got re-assign by event handler. the type of it might chages

@dummdidumm
Copy link
Member

Ah, did not think about that. Thanks!

@dummdidumm dummdidumm added the feature request New feature or request label Jun 22, 2020
@jasonlyu123
Copy link
Member Author

The function callback for props won't work lol. Needs to find another way.

@jasonlyu123
Copy link
Member Author

By the way, this wrapping can be used in $: variable if we change the first $: label to let like we discussed earlier in the overall strategy thread. Because its type is depending on another let. So it would works.

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 24, 2020

Another problem, this union type props will also have a type of string in the exported prop def because of the same reason

@jasonlyu123
Copy link
Member Author

export let someProps : string | number = ''

To

declare function __sveltets__invalidateWithDefault<T>(getValue: () => T | undefined): T;

let someProps: string | number = ''
someProps = __sveltets__invalidateWithDefault(() => someProps);

This works, but kind of hacky

@PatrickG
Copy link
Contributor

export let test: string | number = '' as any;

works

@jasonlyu123
Copy link
Member Author

@PatrickG's solution is better. but both approach would make the props has an undefined type.

export let test: string | number | undefined = '' as any;
test // test is shouldn't be undefined here

@dummdidumm
Copy link
Member

Can you get the types from the typescript AST somehow and use that to construct it? Or use some kind of AST traversal/Regex to get the type definition (everything after : and before =) and then put it behind it: export let test: string | number = '' as string | number.

@PatrickG
Copy link
Contributor

@PatrickG's solution is better. but both approach would make the props has an undefined type.

export let test: string | number | undefined = '' as any;
test // test is shouldn't be undefined here

for me test is string | number. Not undefined.
image

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 29, 2020

playground example
It'll be undefined if strictNullChecks is enabled (strict: true also enable it)

@PatrickG
Copy link
Contributor

playground example
It'll be undefined if strictNullChecks is enabled (strict: true also enable it)

Then it will be string | number | undefined. Where is the problem?

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 29, 2020

Because it can't be undefined when your default value is not undefined. I want to eliminate that undefined type so that users don't have to do an unnecessary null check to suppress the undefined error.

@PatrickG
Copy link
Contributor

PatrickG commented Jun 29, 2020

If you add | undefined to the type, of course it can be undefined and you should check that it is not undefined when you try to invoke a method.

<!-- Component.svelte -->
<script lang="ts">
  export let test: string | undefined = '' as any; // `string | undefined` means it can be undefined.

  test.toUpperCase(); // not safe - because `test` can be `undefined`

  test?.toUpperCase(); // safe
</script>

<!-- Other component -->
<script>
  import Component from './Component.svelte';
</script>

<Componen test={undefined} />

The only Problem I see currently, is that the svelte extension should make the prop optional if it has a initial value.

Instead of checking for undefined in the type here, I think we can use VariableDeclaration.initializer here

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 29, 2020

because svelte compiler compile it to

let { test = '' } = $$props

so test can't be undefined even if you explicitly pass undefined as props. It can only be undefined if you re-assign it to undefined later.

Maybe it would be too complex or impossible to do what I intend to do. And It might be just an edge case that few people would use.

@PatrickG
Copy link
Contributor

PatrickG commented Jun 29, 2020

It still can be undefined if you pass a variable that changes to undefined.
https://svelte.dev/repl/hello-world?version=3.23.2

@jasonlyu123
Copy link
Member Author

Then it's reactive statement that's possibly undefined? Maybe we should just do reactive and require user to explicitly cast the type of default value.

@jasonlyu123
Copy link
Member Author

export let a: string | number | undefined = '' as string | number

Casting can work. but in js it won't have undefined type in a function

    /**@type { string | number | undefined }*/
    let name = /**@type {string | number} */ ("world"),
        world = '';

    name

    function a() {
        name
    }

js playground example

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 29, 2020

Instead of checking for undefined in the type here, I think we can use VariableDeclaration.initializer here

good point! It can differentiate optional props and possibly undefined

@PatrickG
Copy link
Contributor

PatrickG commented Jun 29, 2020

That is what i mean, with the optional props: PatrickG@c5f00f7

And with the as any after the initalizer, vscode no longer thinks export let test: string | number = ''; is a string. PatrickG@7f630bb

Can I create a PR with my changes?

@jasonlyu123
Copy link
Member Author

I think the first one is ok!
Another cons of as any is that It would not get type-check. So maybe it needs to be cast to whatever user has defined.

let a: string | number = true as any;

Also, the casting needs to be done through jsdoc in js. I have tried that today.
About the undefined problem, I give up on it. Since we can't found a generic function that can do the trick. We could only manually check if its initializer is undefined but not if a variable passed in as the initializer is undefined. The false negatives seem to be more important.

@PatrickG
Copy link
Contributor

PatrickG commented Jun 29, 2020

I think the first one is ok!
Another cons of as any is that It would not get type-check. So maybe it needs to be cast to whatever user has defined.

let a: string | number = true as any;

Oh, right.
What about this:

declare function __sveltets_initialize<T>(value: T): any;
let test: string | number = __sveltets_initialize<string | number>('');
test; // string | number

let test: string | number = __sveltets_initialize<string | number>(true); // error because `true` is not `string` or `number`

Also, the casting needs to be done through jsdoc in js. I have tried that today.

I've not looked into the jsdoc stuff yet

@jasonlyu123
Copy link
Member Author

update: there is another way to do as any

export let a: number | string = '1',
  b = a;

to

declare function __sveltets_any(dummy: any): any;

let a: number | string = '1'
a = __sveltets_any(a);
let b = a;

No type-assertion needed and the initializer got type-check. So that this would show typing error:

export let a: ISomeInterface = {};
export let b: string | number = true;

@PatrickG
Copy link
Contributor

I had the re-assignment in my mind too, but wanted to prevent it, because I think it is harder to do^^
Do you implement this?

@jasonlyu123
Copy link
Member Author

jasonlyu123 commented Jun 30, 2020

Yes. I have implemented reassign here but still need some works in cleanup and test.
Another one is focusing on $: variable

dummdidumm pushed a commit that referenced this issue Jul 2, 2020
#214

* infer type from implicit declarations

* test for reactive declaration

* add editorconfig for svelte2tsx test files

* wrap reative block with arrow function

* invalidate control flow type when props is typed and has default

* handle object wrapping

* cleanup

* remove undefined type from invalidated prop

* change to cast default to whatever user has defined

* change to do typ-assertion in reassign

* fix test, cleanup

* rollback infer type from implicit declarations for now

* (fix) var name interpolation
@dummdidumm
Copy link
Member

@jasonlyu123 is everything the issue was about fixed by your PR? (asking for whether or not I can close this)

@jasonlyu123
Copy link
Member Author

Yeah. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants