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

chore(styled): let StyledOptions generic argument be optional #2333

Merged

Conversation

antongolub
Copy link
Contributor

We're now migrating from v10 and noticed a strange thing. StyledOptions now requires Props argument, which does not affect at all. What do you think about either removing the param or at least making it optional?

declare type PropertyKey = string | number | symbol;
...
export interface StyledOptions<Props> {
  label?: string
  shouldForwardProp?(propName: PropertyKey): boolean
  target?: string
}

@changeset-bot
Copy link

changeset-bot bot commented Apr 6, 2021

🦋 Changeset detected

Latest commit: 682313b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@emotion/native Patch
@emotion/styled Patch
@emotion/is-prop-valid Patch

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

@antongolub antongolub changed the title chore(style): let StyledOptions generic argument be optional chore(styled): let StyledOptions generic argument be optional Apr 6, 2021
@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 6, 2021

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:

Sandbox Source
Emotion Configuration

@Andarist
Copy link
Member

Andarist commented Apr 6, 2021

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.

@antongolub
Copy link
Contributor Author

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.
#1643 brought this relaxed type earlier, so now generic seems useless here.

@Andarist
Copy link
Member

Andarist commented Apr 6, 2021

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.

@antongolub
Copy link
Contributor Author

Sounds reasonable. I'll do.

@@ -64,7 +64,7 @@ export interface FilteringStyledOptions<
shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps
}

export interface StyledOptions<Props> {
export interface StyledOptions<Props = Record<PropertyKey, any>> {
Copy link
Member

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

Copy link
Contributor Author

@antongolub antongolub Apr 7, 2021

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?

Copy link
Member

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.

Copy link
Contributor Author

@antongolub antongolub Apr 7, 2021

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
}
  1. No breaking change
  2. Compatible with v10 and v11 codebases
  3. Useful type check

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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 :)

Copy link
Contributor Author

@antongolub antongolub Apr 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist,

Not sure if I found the best solution, but it seems working.

Copy link
Contributor Author

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?

@antongolub antongolub force-pushed the let-styledopts-arg-be-optional branch from d99ba71 to 696213b Compare April 18, 2021 10:50
@antongolub
Copy link
Contributor Author

@Andarist, any suggestions/objections?

@antongolub
Copy link
Contributor Author

@Andarist, let’s try to move this forward.

@Andarist
Copy link
Member

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.

@antongolub
Copy link
Contributor Author

antongolub commented Jun 11, 2021

@Andarist, @mitchellhamilton,

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.

export interface FilteringStyledOptions<
Props,
Props = Record<PropertyKey, any>,
Copy link
Member

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?

Copy link
Contributor Author

@antongolub antongolub Mar 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist

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.

Copy link
Member

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?

Copy link
Contributor Author

@antongolub antongolub Mar 9, 2022

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.

Copy link
Contributor Author

@antongolub antongolub Mar 9, 2022

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)

Copy link
Member

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)

@Andarist
Copy link
Member

So it seems like the only thing to settle on is the variance type for the shouldForwardProp. Let me know what do you think about this.

👋

@Andarist
Copy link
Member

Andarist commented Mar 5, 2022

FYI just 2 small questions to be answered here and we should be able to land this soon. No rush, of course 😅

/** 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.
Copy link
Member

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.

@@ -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).
Copy link
Member

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.

@@ -2,7 +2,11 @@
// TypeScript Version: 3.2

import * as React from 'react'
import { ComponentSelector, Interpolation } from '@emotion/serialize'
import {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can revert this change

@Andarist Andarist force-pushed the let-styledopts-arg-be-optional branch from 6e308de to cbf28fa Compare May 30, 2022 15:04
@Andarist Andarist force-pushed the let-styledopts-arg-be-optional branch from cbf28fa to f360473 Compare May 30, 2022 15:49
@Andarist
Copy link
Member

Andarist commented Jun 5, 2022

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 (string) for the shouldForwardProp's parameter is not ideal but given how our types are structured I feel like it's the only way to match type-safety expectations.

Copy link
Member

@srmagura srmagura left a 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.

/** 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.
Copy link
Member

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
Copy link
Member

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AdditionalProps are declared here:

<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.

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

Successfully merging this pull request may close these issues.

None yet

3 participants