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
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ccc955d
chore(style): let StyledOptions generic argument be optional
antongolub Apr 6, 2021
29ca437
chore(native): let StyledOptions generic argument be optional
antongolub Apr 6, 2021
107e017
test(types): add tests for StyledOptions iface
antongolub Apr 7, 2021
d163c00
feat(types): add keys assertion for StyledOptions generic
antongolub Apr 7, 2021
4c5b17e
fix(styled): add StyledOptions type to CreateStyledComponent call sig…
antongolub Apr 9, 2021
659f6ec
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Apr 14, 2021
165a422
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Apr 16, 2021
696213b
test(styled): check type StyledOpts for composite component
antongolub Apr 18, 2021
fae2dab
test(styled): revert erroneous changes
antongolub Apr 19, 2021
2238e9e
test(styled): clarify StyledOptsBroken test case
antongolub Apr 19, 2021
a1775f8
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub May 9, 2021
e36ff17
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub May 29, 2021
009b7b0
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Jun 7, 2021
b2e0b7a
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Aug 10, 2021
b9ecb17
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Oct 23, 2021
1cc73a2
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Nov 8, 2021
34be1e2
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Nov 15, 2021
04e367b
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Nov 23, 2021
310dba8
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Dec 16, 2021
b8d3039
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Dec 25, 2021
5286a60
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Jan 7, 2022
d889b0e
test: refactor `styledOptions` generics tests
antongolub Jan 12, 2022
0fa6ee4
test: add more tests for StyledOptions
antongolub Jan 12, 2022
83fae39
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Jan 12, 2022
ebfbd9a
test: linting
antongolub Jan 12, 2022
9e78345
test: tweak ups
antongolub Jan 12, 2022
894aa3b
feat: expose FilteringStyledOptions
antongolub Jan 14, 2022
43f318b
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Jan 31, 2022
27b4f2d
fix: expost `FilteringStyledOptions` from native, tweak up some tests…
antongolub Feb 25, 2022
c2c2c27
Merge branch 'main' into let-styledopts-arg-be-optional
antongolub Feb 25, 2022
d3092bb
chore: use consistent default props type for both StyledOptions and F…
antongolub Mar 8, 2022
4104c2a
chore: snap #3
antongolub Mar 9, 2022
5595c8d
styled.shouldForwardProp: require prop to be a string
srmagura May 21, 2022
09df01d
Add PropertyKey->string changesets
srmagura May 21, 2022
5f3f5e5
Remove redundant `compilerOptions`
Andarist May 21, 2022
da3a461
make `shouldForwardProp` always contravariant
Andarist May 22, 2022
a4c2a75
Merge branch 'main' into let-styledopts-arg-be-optional
Andarist May 23, 2022
d133a83
Merge branch 'main' into let-styledopts-arg-be-optional
Andarist May 30, 2022
f360473
Type fixes
Andarist May 30, 2022
ac3f8c7
Tweak `FilteringStyledOptions` and `StyledOptions` interfaces
Andarist May 30, 2022
fce092c
Change `Props` constraint
Andarist May 30, 2022
59e8aec
Tweak type-level tests around `shouldForwardProp`
Andarist May 30, 2022
65f4444
Merge remote-tracking branch 'srmagura/styled-prop-string' into let-s…
Andarist Jun 5, 2022
682313b
add changesets
Andarist Jun 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/native/types/base.d.ts
Expand Up @@ -58,16 +58,19 @@ export type Interpolation<
| ArrayInterpolation<MergedProps, StyleType>
| FunctionInterpolation<MergedProps, StyleType>

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

*/
export interface FilteringStyledOptions<
Props,
Props = Record<PropertyKey, any>,
ForwardedProps extends keyof Props = keyof Props
> {
shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps
}

export interface StyledOptions<Props> {
shouldForwardProp?(propName: PropertyKey): boolean
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?

shouldForwardProp?(propName: keyof Props): boolean
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/native/types/index.d.ts
Expand Up @@ -18,6 +18,7 @@ export {
ArrayInterpolation,
CreateStyledComponent,
CSSInterpolation,
FilteringStyledOptions,
FunctionInterpolation,
Interpolation,
InterpolationPrimitive,
Expand Down
20 changes: 19 additions & 1 deletion packages/native/types/tests.tsx
Expand Up @@ -7,7 +7,12 @@ import {
TextStyle,
View
} from 'react-native'
import styled, { css, ReactNativeStyle } from '@emotion/native'
import styled, {
css,
ReactNativeStyle,
StyledOptions,
FilteringStyledOptions
} from '@emotion/native'

declare module '@emotion/react' {
// tslint:disable-next-line: strict-export-declare-modifiers
Expand Down Expand Up @@ -166,3 +171,16 @@ export const ImageFullWidthContained = styled.Image`
;<Container1 ref={containerRef1} />
;<Container2 ref={containerRef2} />
}

{
// Props forwarding through StyledOptions and FilteringStyledOptions

styled(View, { shouldForwardProp: (prop: 'testID') => true })({})

styled(View, {
shouldForwardProp: (prop: 'testID'): prop is 'testID' => true
})({})

// $ExpectError
styled(View, { shouldForwardProp: (prop: 'foo') => true })({})
}
4 changes: 4 additions & 0 deletions packages/native/types/tsconfig.json
Expand Up @@ -6,6 +6,10 @@
"lib": ["es6", "dom"],
"module": "commonjs",
"noEmit": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
Andarist marked this conversation as resolved.
Show resolved Hide resolved
"strict": true,
"target": "es5",
"typeRoots": ["../"],
Expand Down
17 changes: 12 additions & 5 deletions packages/styled/types/base.d.ts
Expand Up @@ -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

ComponentSelector,
Interpolation,
InterpolationPrimitive
} from '@emotion/serialize'
import { PropsOf, DistributiveOmit, Theme } from '@emotion/react'

export {
Expand All @@ -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.

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

*/
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)

ForwardedProps extends keyof Props = keyof Props
> {
label?: string
shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps
target?: string
}

export interface StyledOptions<Props> {
export interface StyledOptions<Props = Record<PropertyKey, any>> {
label?: string
shouldForwardProp?(propName: PropertyKey): boolean
shouldForwardProp?(propName: keyof Props): boolean
target?: string
}

Expand Down
1 change: 1 addition & 0 deletions packages/styled/types/index.d.ts
Expand Up @@ -12,6 +12,7 @@ export {
Interpolation,
StyledComponent,
StyledOptions,
FilteringStyledOptions,
CreateStyledComponent
} from './base'

Expand Down
68 changes: 67 additions & 1 deletion packages/styled/types/tests.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
import styled from '@emotion/styled'
import styled, { StyledOptions, FilteringStyledOptions } from '@emotion/styled'

// This file uses the same Theme declaration from tests-base.tsx

Expand Down Expand Up @@ -217,3 +217,69 @@ const Input5 = styled.input`
// $ExpectError
;<StyledCompWithoutAs as={Section} />
}

{
// Props forwarding through StyledOptions and FilteringStyledOptions

const fc: React.FC<{ foo: string }> = props => <div {...props} />

styled(fc, { shouldForwardProp: (prop: 'foo') => true })({})
Andarist marked this conversation as resolved.
Show resolved Hide resolved

styled(fc, { shouldForwardProp: (prop: string) => true })({})

// $ExpectError
styled(fc, { shouldForwardProp: (prop: 'bar') => true })({})

const shouldForwardProp1 = (prop: 'foo') => true
styled(fc, { shouldForwardProp: shouldForwardProp1 })({})
Copy link
Member

Choose a reason for hiding this comment

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

The same doubt about this one as here


const shouldForwardProp2: StyledOptions['shouldForwardProp'] = (
prop: 'unknown'
) => true
styled(fc, { shouldForwardProp: shouldForwardProp2 })({})

const shouldForwardProp3 = (prop: 'unknown') => true
// $ExpectError
styled(fc, { shouldForwardProp: shouldForwardProp3 })({})

// $ExpectError
const shouldForwardProp4: StyledOptions<{
foo: string
}>['shouldForwardProp'] = (prop: 'unknown') => true

const shouldForwardProp5 = (prop: 'foo'): prop is 'foo' => true
styled(fc, { shouldForwardProp: shouldForwardProp5 })({})

const shouldForwardProp6: FilteringStyledOptions['shouldForwardProp'] = (
prop: 'foo'
): prop is 'foo' => true

const shouldForwardProp7: FilteringStyledOptions<{
foo: string
}>['shouldForwardProp'] = (prop: 'foo'): prop is 'foo' => true

// $ExpectError
const shouldForwardProp8: FilteringStyledOptions<{
foo: string
}>['shouldForwardProp'] = (prop: 'unknown'): prop is 'unknown' => true

const shouldForwardProp9: FilteringStyledOptions<
{ foo: string; bar: string },
'foo'
>['shouldForwardProp'] = (prop: 'foo' | 'bar'): prop is 'foo' => true

// $ExpectError
const shouldForwardProp10: FilteringStyledOptions<
{ foo: string; bar: string },
'foo'
>['shouldForwardProp'] = (prop: 'foo' | 'bar'): prop is 'bar' => true

styled('div', { shouldForwardProp: (prop: 'color') => true })({})

styled('div', {
shouldForwardProp: (prop: 'color'): prop is 'color' => true
})({})

// $ExpectError
styled('div', { shouldForwardProp: (prop: 'foo') => true })({})
}
4 changes: 4 additions & 0 deletions packages/styled/types/tsconfig.json
Expand Up @@ -7,6 +7,10 @@
"lib": ["es6", "dom"],
"module": "commonjs",
"noEmit": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
Andarist marked this conversation as resolved.
Show resolved Hide resolved
"strict": true,
"target": "es5",
"typeRoots": ["../"],
Expand Down