diff --git a/package.json b/package.json index 29c0423..b60af5a 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "start:build": "yarn build && yarn example build", "start:debug": "yarn build && yarn example debug", "test": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --experimental-vm-modules\" jest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit && tsc --noEmit -p src/__tests__/types" }, "keywords": [ "react-native", diff --git a/src/__tests__/types/_exact-optional-props.tsx b/src/__tests__/types/_exact-optional-props.tsx new file mode 100644 index 0000000..85bd7d0 --- /dev/null +++ b/src/__tests__/types/_exact-optional-props.tsx @@ -0,0 +1,103 @@ +import type { + ButtonProps, + FlatListProps, + ImageBackgroundProps, + ImageProps, + InputAccessoryViewProps, + KeyboardAvoidingViewProps, + ModalProps, + ScrollViewProps, + StatusBarProps, + SwitchProps, + TextInputProps, + TextProps, + TouchableWithoutFeedbackProps, + ViewProps, +} from "react-native"; + +import type { VirtualizedListWithoutRenderItemProps } from "@react-native/virtualized-lists"; + +// Every prop this package adds must accept an explicit `undefined`, as React Native's own +// optional props already do. Under `exactOptionalPropertyTypes` a bare `?: string` rejects +// it, so `className={condition ? "p-4" : undefined}` — the ordinary conditional — fails. +// This file compiles with that flag on; the root typecheck cannot observe the difference. +declare const maybeString: string | undefined; +declare const maybeBoolean: boolean | undefined; +declare const noop: () => void; + +export const view: ViewProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const text: TextProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const image: ImageProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const switchProps: SwitchProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const inputAccessoryView: InputAccessoryViewProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const touchableWithoutFeedback: TouchableWithoutFeedbackProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const statusBar: StatusBarProps = { + className: maybeString, + cssInterop: maybeBoolean, +}; + +export const button: ButtonProps = { + title: "", + onPress: noop, + className: maybeString, +}; + +export const scrollView: ScrollViewProps = { + contentContainerClassName: maybeString, + indicatorClassName: maybeString, +}; + +export const flatList: FlatListProps = { + data: [], + renderItem: () => null, + columnWrapperClassName: maybeString, +}; + +export const imageBackground: ImageBackgroundProps = { + source: 0, + imageClassName: maybeString, +}; + +export const textInput: TextInputProps = { + placeholderClassName: maybeString, +}; + +export const keyboardAvoidingView: KeyboardAvoidingViewProps = { + contentContainerClassName: maybeString, +}; + +export const modal: ModalProps = { + presentationClassName: maybeString, +}; + +export const virtualizedList: VirtualizedListWithoutRenderItemProps = { + data: [], + getItem: () => undefined, + getItemCount: () => 0, + ListFooterComponentClassName: maybeString, + ListHeaderComponentClassName: maybeString, +}; diff --git a/src/__tests__/types/tsconfig.json b/src/__tests__/types/tsconfig.json new file mode 100644 index 0000000..bef1691 --- /dev/null +++ b/src/__tests__/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "exactOptionalPropertyTypes": true + }, + "include": ["./*"], + "files": ["../../../types.d.ts"] +} diff --git a/src/runtime.types.ts b/src/runtime.types.ts index 6e8f9ad..ec24d04 100644 --- a/src/runtime.types.ts +++ b/src/runtime.types.ts @@ -30,7 +30,7 @@ export type StyledReactElement< : M[K] extends true | string | object ? K : never - : never]?: string; + : never]?: string | undefined; } >; @@ -41,7 +41,7 @@ export type StyledProps> = P & { : M[K] extends true | string | object ? K : never - : never]?: string; + : never]?: string | undefined; }; export type Styled = < @@ -64,7 +64,7 @@ type StyledComponent< : M[K] extends true | string | object ? K : never - : never]?: string; + : never]?: string | undefined; } >; diff --git a/types.d.ts b/types.d.ts index 1ba5b85..2b359e3 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,4 +1,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +// Every added prop is declared `| undefined` explicitly so the declarations +// hold under `exactOptionalPropertyTypes`, where `?: string` forbids passing an +// explicit `undefined` — which is what a conditional `className={x ? a : undefined}` +// spreads, and what every optional prop in React Native's own types already allows. import type { ScrollViewProps, ScrollViewPropsAndroid, @@ -10,64 +14,64 @@ import type { declare module "@react-native/virtualized-lists" { export interface VirtualizedListWithoutRenderItemProps extends ScrollViewProps { - ListFooterComponentClassName?: string; - ListHeaderComponentClassName?: string; + ListFooterComponentClassName?: string | undefined; + ListHeaderComponentClassName?: string | undefined; } } declare module "react-native" { interface ButtonProps { - className?: string; + className?: string | undefined; } interface ScrollViewProps extends ViewProps, ScrollViewPropsIOS, ScrollViewPropsAndroid, Touchable { - contentContainerClassName?: string; - indicatorClassName?: string; + contentContainerClassName?: string | undefined; + indicatorClassName?: string | undefined; } interface FlatListProps extends VirtualizedListProps { - columnWrapperClassName?: string; + columnWrapperClassName?: string | undefined; } interface ImageBackgroundProps extends ImagePropsBase { - imageClassName?: string; + imageClassName?: string | undefined; } interface ImagePropsBase { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface ViewProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface TextInputProps { - placeholderClassName?: string; + placeholderClassName?: string | undefined; } interface TextProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface SwitchProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface InputAccessoryViewProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface TouchableWithoutFeedbackProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface StatusBarProps { - className?: string; - cssInterop?: boolean; + className?: string | undefined; + cssInterop?: boolean | undefined; } interface KeyboardAvoidingViewProps extends ViewProps { - contentContainerClassName?: string; + contentContainerClassName?: string | undefined; } interface ModalBaseProps { - presentationClassName?: string; + presentationClassName?: string | undefined; } }