From 7b9255b8b65396e8ac205f087b0170f16b3df704 Mon Sep 17 00:00:00 2001 From: Ramya Ramanathan Date: Wed, 7 Oct 2026 07:36:31 -0700 Subject: [PATCH] Make legacy TypeScript ImageStyle extend ViewStyle (#58907) Summary: Fixes #52957 ## Motivation Projects on the default legacy TypeScript types get type errors when they set valid style props on ``. This is coming from [issue](https://github.com/facebook/react-native/issues/52957) ## What is the Issue About? It reports that `filter`, `boxShadow`, `mixBlendMode`, `borderTopStartRadius` and `outlineColor` props give TS error. These props are already accepted by the opt-in strict API types. ## Background React Native ships two sets of TypeScript style types: - **Legacy (default):** hand-written in `types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts`. Maintained by hand, so it drifts. - **Strict API (opt-in):** generated from the Flow source of truth, `Libraries/StyleSheet/StyleSheetTypes.js`, so it cannot drift. ## Root Cause In Flow, `ImageStyle` spreads `ViewStyle` and only narrows `overflow` to `visible | hidden`. The legacy `ImageStyle` instead extended `FlexStyle`/`ShadowStyleIOS`/`TransformsStyle` and redeclared a hand-picked subset of view props, so it rejected 37 `ViewStyle` props: - Border and outline colours: `borderTopColor`, `borderBottomColor`, `borderLeftColor`, `borderRightColor`, `borderStartColor`, `borderEndColor`, `borderBlockColor`, `borderBlockStartColor`, `borderBlockEndColor`, `outlineColor` - Logical corner radii: `borderTopStartRadius`, `borderTopEndRadius`, `borderBottomStartRadius`, `borderBottomEndRadius`, `borderStartStartRadius`, `borderStartEndRadius`, `borderEndStartRadius`, `borderEndEndRadius` - `borderCurve`, `borderStyle`, `outlineStyle`, `outlineWidth`, `outlineOffset`, `elevation`, `pointerEvents`, `isolation`, `boxShadow`, `filter`, `mixBlendMode` - Background layers: `backgroundImage`, `backgroundSize`, `backgroundPosition`, `backgroundRepeat` and their `experimental_` versions `TextStyle` already extends `ViewStyle`, so it is not affected. ## Description of Changes - `ImageStyle` now extends `ViewStyle`, narrows `overflow` to `visible | hidden`, and keeps only the Image-specific props (`resizeMode`, `overlayColor`, `tintColor`, `objectFit`). The 10 view props it used to redeclare are now inherited with identical types. New `ViewStyle` props flow through automatically, as they already do for `TextStyle` and for the generated types. - The type test `__typetests__/stylesheet-style-props.tsx` (renamed from the earlier issue reproducer) sets all 37 props on an Image style. It asserts that `ImageStyle` and `TextStyle` declare every `ViewStyle` key, so future drift fails CI with the missing prop names. Negative checks confirm `ImageStyle` still rejects `overflow: "scroll"` and text-only props. The change only widens what `ImageStyle` accepts, matching the generated types. Changelog: [General][Fixed] - Fix the legacy TypeScript `ImageStyle` type rejecting valid `ViewStyle` props such as `filter`, `boxShadow` and `mixBlendMode` Reviewed By: vzaidman Differential Revision: D123003237 --- .../__typetests__/stylesheet-style-props.tsx | 104 ++++++++++++++++++ .../Libraries/StyleSheet/StyleSheetTypes.d.ts | 12 +- 2 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 packages/react-native/__typetests__/stylesheet-style-props.tsx diff --git a/packages/react-native/__typetests__/stylesheet-style-props.tsx b/packages/react-native/__typetests__/stylesheet-style-props.tsx new file mode 100644 index 000000000000..30d412b06593 --- /dev/null +++ b/packages/react-native/__typetests__/stylesheet-style-props.tsx @@ -0,0 +1,104 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// The hand-written legacy ImageStyle and TextStyle must accept every ViewStyle +// prop, as the generated (strict API) types do. ImageStyle only narrows +// `overflow`. See https://github.com/facebook/react-native/issues/52957 + +import * as React from 'react'; +// @ts-ignore +import { + Image, + View, + type ImageStyle, + type TextStyle, + type ViewStyle, +} from 'react-native'; + +type MissingKeys = Exclude; + +// On failure, the error names every ViewStyle prop the target type lacks. +const imageStyleHasAllViewStyleKeys: [ + MissingKeys>, +] extends [never] + ? true + : MissingKeys> = true; + +const textStyleHasAllViewStyleKeys: [ + MissingKeys, +] extends [never] + ? true + : MissingKeys = true; + +function viewStyleToImageStyle(style: Omit): ImageStyle { + return style; +} + +const imageStyle: ImageStyle = { + borderBlockColor: 'red', + borderBlockEndColor: 'red', + borderBlockStartColor: 'red', + borderBottomColor: 'red', + borderEndColor: 'red', + borderLeftColor: 'red', + borderRightColor: 'red', + borderStartColor: 'red', + borderTopColor: 'red', + borderBottomEndRadius: 4, + borderBottomStartRadius: 4, + borderEndEndRadius: 4, + borderEndStartRadius: 4, + borderStartEndRadius: 4, + borderStartStartRadius: 4, + borderTopEndRadius: 4, + borderTopStartRadius: 4, + borderCurve: 'continuous', + borderStyle: 'dashed', + outlineColor: 'blue', + outlineOffset: 2, + outlineStyle: 'solid', + outlineWidth: 1, + elevation: 2, + pointerEvents: 'none', + isolation: 'isolate', + boxShadow: '0 2px 4px black', + filter: 'blur(4px)', + mixBlendMode: 'multiply', + backgroundImage: 'linear-gradient(red, blue)', + experimental_backgroundImage: 'linear-gradient(red, blue)', + backgroundSize: '50% 50%', + experimental_backgroundSize: '50% 50%', + backgroundPosition: 'center', + experimental_backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + experimental_backgroundRepeat: 'no-repeat', +}; + +const imageStyleWithScrollOverflow: ImageStyle = { + // @ts-expect-error Image does not support `overflow: 'scroll'`. + overflow: 'scroll', +}; + +const imageStyleWithTextProp: ImageStyle = { + // @ts-expect-error `fontSize` is not an Image style prop. + fontSize: 12, +}; + +const viewStyle: ViewStyle = { + backgroundColor: 'white', +}; + +export function App() { + return ( + + + + + ); +} diff --git a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts index 4aa9f68b437d..478e9d092195 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -649,20 +649,10 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle { * Image style * @see https://reactnative.dev/docs/image#style */ -export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { +export interface ImageStyle extends ViewStyle { resizeMode?: ImageResizeMode | undefined; - backfaceVisibility?: 'visible' | 'hidden' | undefined; - borderBottomLeftRadius?: AnimatableNumericValue | string | undefined; - borderBottomRightRadius?: AnimatableNumericValue | string | undefined; - backgroundColor?: ColorValue | undefined; - borderColor?: ColorValue | undefined; - borderRadius?: AnimatableNumericValue | string | undefined; - borderTopLeftRadius?: AnimatableNumericValue | string | undefined; - borderTopRightRadius?: AnimatableNumericValue | string | undefined; overflow?: 'visible' | 'hidden' | undefined; overlayColor?: ColorValue | undefined; tintColor?: ColorValue | undefined; - opacity?: AnimatableNumericValue | undefined; objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none' | undefined; - cursor?: CursorValue | undefined; }