-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
chore(styled): let StyledOptions generic argument be optional #2333
chore(styled): let StyledOptions generic argument be optional #2333
Conversation
🦋 Changeset detectedLatest commit: 682313b The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 682313b:
|
Please prepare a runnable repro case of your problem or add a type test (in the types directory) that would showcase why this is needed. |
Hey, @Andarist, This is not a problem. Once we've bumped emotion version in our project, we just found API diff, which does not add any functionality. |
Hm, it should still be possible to add a test type for this - since some code did not work for you and you expect it to work. |
Sounds reasonable. I'll do. |
packages/native/types/base.d.ts
Outdated
@@ -64,7 +64,7 @@ export interface FilteringStyledOptions< | |||
shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps | |||
} | |||
|
|||
export interface StyledOptions<Props> { | |||
export interface StyledOptions<Props = Record<PropertyKey, any>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, this generic stays unused so I think it should be more than OK to just drop it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I suggest using this impl cause it's compatible with both v10 and v11-based app code. Removing generic will be a breaking change for someone. Is it acceptable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any change is a breaking change for someone 😅 I think in this case it's a pragmatic choice to not treat it as one - especially give that I don't expect StyledOptions
to be used explicitly in a lot of codebases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to use generic argument if passed?
export interface StyledOptions<Props = null> {
label?: string
shouldForwardProp?(propName: Props extends null ? PropertyKey : keyof Props): boolean
target?: string
}
- No breaking change
- Compatible with v10 and v11 codebases
- Useful type check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quite honestly, I'm not sure if it's worth it. This type should not be used that much publicly and we'd have to check if this would be compatible when passed to styled
calls. It would also be good to dig up the PRs by @JakeGinnivan which were related to this and make sure that this would not break because of the reasons mentioned there. Ideally, we would pass Props
to StyledOptions
from within CreateStyledComponent
types.
If you are willing to do the work - then sure, ye, we could try to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if this is supposed to land then I strongly believe that those 2 cases should not result in any type errors - even if the problem is not related to this PR it IMHO would be preferable to fix it here or prepare a separate fix, land it and then rebase this PR. Without those working it's hard for me to justify merging this PR with that generic being used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Give me a bit more time. I'll fix the issue above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem - there is no rush. I appreciate your efforts here :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I found the best solution, but it seems working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Andarist,
Could you take a look?
d99ba71
to
696213b
Compare
@Andarist, any suggestions/objections? |
@Andarist, let’s try to move this forward. |
Sorry for the delay but I was lately very busy. Please be patient - I'll get to reviewing this PR when I get some time for it. |
Do you need any help with this? Maybe I should change or add something to make it easier for review? We are really looking forward to this PR change. |
packages/styled/types/base.d.ts
Outdated
export interface FilteringStyledOptions< | ||
Props, | ||
Props = Record<PropertyKey, any>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: why the Props
have a different default between FilteringStyledOptions
and StyledOptions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Props = Record<PropertyKey, any>,
This is just an artifact of the first impl. I'll fix.
So it seems like the only thing to settle on is the variance type for the
shouldForwardProp
.
It seems that strictFunctionTypes
option is not applied to object method?() {...}
signature. So if we rewrite the code as suggested above (method?: () => {...}
) we break everything.
export interface FilteringStyledOptions<
Props = AnyRecord,
ForwardedProps extends keyof Props = keyof Props
> {
label?: string
shouldForwardProp?: <P extends UnionToIntersection<ForwardedProps>>(propName: P) => propName is P
target?: string
}
export interface StyledOptions<Props = AnyRecord> {
label?: string
shouldForwardProp?: <P extends keyof UnionToIntersection<Props>>(propName: P) => boolean
target?: string
}
type AnyRecord = Record<PropertyKey, any>
export type UnionToIntersection<U, K = any> = (
U extends K ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
The main problem: I don't know how to infer param type inside generic, so I cannot add a type assertion that impl fn param extends HTML element attributes.
type F = <P extends any extends infer P ? P : never>(p: P) => boolean
// P is unknown
const f: F = (prop: 'style') => true
I also tried to handle this in another way: to represent shouldForwardProp
as a union of fns with all possible signatures. I looks syntactically correct and automatable with recursive mapping, but it requires a quantum computer.
type T = 'a' | 'b' | 'c'
type F = (p: 'a') => true | (p: 'b') => true | (p: 'c') => true
| (p: 'a' | 'b') => true | (p: 'b' | 'c') => true | (p: 'a' | 'c') => true
| (p: 'a' | 'b' | 'c') =>
For ~300 attributed for every HTML element type we get: C(n,k) = n! / k!(n-k)!, k = 1, n=300, C =~3E615
.
https://www.numberempire.com/factorialcalculator.php?number=300
PS Sorry for the late reply.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that strictFunctionTypes option is not applied to object method?: () {} signature
If I understand this correctly then with strictFunctionTypes: false
every function is bivariant but with strictFunctionTypes: true
methods are still bivariant but every other function becomes contravariant.
I think you might have a typo in this example as it should be method?(): void
(or similar) to fit the rest of the sentence. I've just wanted to clarify that we have a common understanding of this stuff (where I'm not 100% sure if my understanding is fully correct).
So if we rewrite the code as suggested above we break everything.
Does "as suggested" mean "change to a function type and thus make it contravariant"? Could you give me an example of what this would break? If it breaks something important then I think we should have a type test for this.
shouldForwardProp?: <P extends UnionToIntersection>(propName: P) => propName is P
I'm unsure what's happening here. I understand what UnionToIntersection
does but I don't see why it was applied here. The result of this type is an intersection and P
definitely should be constrained to a union of strings (or to just a string type). Could you explain what was the goal here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed several snapshot branches so you can observe my poor efforts:
- snap#3 All gneric singatures use
Record<PropertyKey, any>
as default type. Filters types exposed as object fields. The only working. - snap#2
shouldForwardProp
type declared as a generic function +UnionToIntersection
- snap#1 same as 2 but w/o
UnionToIntersection
.
UnionToIntersection
was placed here to ensure that keyof type includes all the keys of all types of Props
union:
ClassAttributes<HTMLDivElement> | HTMLAttributes<HTMLDivElement> → keyof ClassAttributes<HTMLDivElement> | keyof HTMLAttributes<HTMLDivElement>
. Just as experiment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that we ran into some limitation (in understanding?) of the current TS. What I want to get:
// Declare filter signature that operates the keys of set 'a' | 'b' ... 'z'
type F {
filter: (p: 'a' | 'b' ... 'z') => bool
}
// Introduce a specific filter, that uses a subset p type
const f: F = {
filter: (p: 'a' | 'c') => bool
}
// 'a' | 'c' extends 'a' | 'c' ... 'z'
btw, tsconfig changes were required by dtslint
:
% dtslint types
Error: Expected `"strictFunctionTypes": true` or `"strictFunctionTypes": false`.
at Object.<anonymous> (/Users/a.golub/projects/gh/emotion/node_modules/dtslint/bin/checks.js:95:23)
at Generator.next (<anonymous>)
at fulfilled (/Users/a.golub/projects/gh/emotion/node_modules/dtslint/bin/checks.js:4:58)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed several snapshot branches so you can observe my poor efforts:
Thank you for those - I'll try to review them this week to get a better feeling of what we are dealing with here.
UnionToIntersection was placed here to ensure that keyof type includes all the keys of all types of Props union:
Do we need to care about ClassAttributes
though? This interface only contains key
and ref
properties and those can't really b received by shouldForwardProp
as those are special props in React and it won't ever handle them to us.
It seems that we ran into some limitation (in understanding?) of the current TS. What I want to get:
Ye, this is impossible - mainly because now you could make such a call f.filter('z')
and it could potentially crash at runtime as the supplied implementation is not capable of handling 'z'
.
So basically, here, assigning to this type is not valid. Such things could be expressed with some function helpers and stuff, like here:
TS playground
The gist of this is that we preserve the type of the implementation function but we check if it's valid according to our rules. However, I'm not sure when this would be needed/helpful in our case - probably a full example involving those filters and a styled
call would help me to see this (maybe it's already in those branches of yours, I didn't yet have a chance to look there though)
👋 |
FYI just 2 small questions to be answered here and we should be able to land this soon. No rush, of course 😅 |
…ilteringStyledOptions
packages/styled/types/base.d.ts
Outdated
/** Same as StyledOptions but shouldForwardProp must be a type guard */ | ||
/** | ||
* Same as StyledOptions but shouldForwardProp must be a type guard (https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). | ||
* Practical sense: you can define and reuse atomic `shouldForwardProp` filters that are strictly bound with some `ForwardedProps` type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this line is saying... maybe rewording it or including an example would be helpful.
packages/styled/types/base.d.ts
Outdated
@@ -13,19 +17,22 @@ export { | |||
|
|||
export { ComponentSelector, Interpolation } | |||
|
|||
/** Same as StyledOptions but shouldForwardProp must be a type guard */ | |||
/** | |||
* Same as StyledOptions but shouldForwardProp must be a type guard (https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this link to the TS handbook is necessary, and it will eventually become outdated if they release a new version of the handbook.
packages/styled/types/base.d.ts
Outdated
@@ -2,7 +2,11 @@ | |||
// TypeScript Version: 3.2 | |||
|
|||
import * as React from 'react' | |||
import { ComponentSelector, Interpolation } from '@emotion/serialize' | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can revert this change
6e308de
to
cbf28fa
Compare
cbf28fa
to
f360473
Compare
…tyledopts-arg-be-optional # Conflicts: # packages/native/types/base.d.ts # packages/styled/types/base.d.ts
I will wait for the CI results and I still need to add some changesets here but other than that this is almost ready to land. If you have any objections to the final shape of this PR - this is the time to raise them. The situation here is quite convoluted and also the ultra-wide type ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks good to me, though I am not super confident since this is a complex change. My only comments were about cleaning up some of the comments in the code.
packages/native/types/base.d.ts
Outdated
/** Same as StyledOptions but shouldForwardProp must be a type guard */ | ||
/** | ||
* Same as StyledOptions but shouldForwardProp must be a type guard (https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates). | ||
* Practical sense: you can define and reuse atomic `shouldForwardProp` filters that are strictly bound with some `ForwardedProps` type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comments on the version of this in packages/styled/types/base.d.ts
.
const fc: React.FC<{ foo: string }> = () => null | ||
|
||
// we can't accept a "known" prop here because we need to include `AdditionalProps` and those aren't available yet | ||
// `Props` represent the actual props of a component while `AdditionalProps` represent props used only for styling purposes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is AdditionalProps
referring to here? Maybe it is supposed to say ForwardedProps
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AdditionalProps
are declared here:
emotion/packages/styled/types/base.d.ts
Line 72 in 65f4444
<AdditionalProps extends {} = {}>( |
The problem is this:
styled<GenericsThatDontHaveAccessToAdditionalProps>('div', {
shouldForwardProp: (prop: ThisHasToBeAbleToAcceptAdditionalProps) => true
})<AdditionalProps>({
color: (props) => props.myAwesomeAdditionalColor
})
As we might see here - AdditionalProps
are provided to what is being returned from styled
but yet shouldForwardProp
has only access to what is given to styled
.
We're now migrating from v10 and noticed a strange thing.
StyledOptions
now requiresProps
argument, which does not affect at all. What do you think about either removing the param or at least making it optional?