From e869378b754c080f55db11aea84378ebf434d818 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Sun, 2 Aug 2026 00:48:24 +0300 Subject: [PATCH 1/2] fix: accept an explicit undefined on every prop this package adds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under `exactOptionalPropertyTypes`, a bare `?: string` FORBIDS passing an explicit `undefined` — and `className={condition ? 'a' : undefined}` is exactly what a conditional class spreads. So every prop this package adds to React Native's own interfaces rejected a value React Native's own optional props accept, at every such call site. Each added declaration is widened to `| undefined`. No runtime behaviour changes; this is the declaration matching what the implementation already allowed. --- types.d.ts | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/types.d.ts b/types.d.ts index 1ba5b856..2b359e37 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; } } From 6c6da9445046feb7cba5955e31cb3bfdfc3963d6 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 14 Aug 2026 21:14:39 +0300 Subject: [PATCH 2/2] fix(types): accept an explicit undefined on every prop this package adds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React Native's own optional props are declared `| undefined`; the props this package augments them with are not. Under `exactOptionalPropertyTypes` that makes `className={condition ? "p-4" : undefined}` — the ordinary conditional — a type error, and there is no way for a consumer to fix it except by patching the package. `src/runtime.types.ts` needs the same treatment. Its three mapped types re-narrow className to a bare optional, so `react-native-css/components` and `styled()` — the imports the README documents — stay broken even once `types.d.ts` is fixed. The contract is machine-checked. `src/__tests__/types` is a two-file program that compiles the fixture against `types.d.ts` with the flag on; reverting `types.d.ts` turns it red with 15 errors while the root typecheck stays green, which is the point — the existing typecheck structurally cannot observe this. --- package.json | 2 +- src/__tests__/types/_exact-optional-props.tsx | 103 ++++++++++++++++++ src/__tests__/types/tsconfig.json | 8 ++ src/runtime.types.ts | 6 +- 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/types/_exact-optional-props.tsx create mode 100644 src/__tests__/types/tsconfig.json diff --git a/package.json b/package.json index 29c04232..b60af5aa 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 00000000..85bd7d07 --- /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 00000000..bef16916 --- /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 6e8f9ad8..ec24d04f 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; } >;