diff --git a/src/__tests__/native/styled.test.ios.tsx b/src/__tests__/native/styled.test.ios.tsx index 3f4ef620..21e55876 100644 --- a/src/__tests__/native/styled.test.ios.tsx +++ b/src/__tests__/native/styled.test.ios.tsx @@ -163,6 +163,25 @@ test.each(["nativeStyleMapping", "nativeStyleToProp"] as const)( }, ); +test("a mapping entry set to undefined maps nothing", () => { + registerCSS(`.subject { color: blue; background-color: red; }`); + const StyledView = styled(View, { + className: { + target: "style", + nativeStyleMapping: { + color: "accessibilityLabel", + backgroundColor: undefined, + }, + }, + }); + + render(); + + const props = screen.getByTestId(testID).props; + expect(props.accessibilityLabel).toBe("#00f"); + expect(props.style).toEqual({ backgroundColor: "#f00" }); +}); + test.each([false, true])( "current mapping wins over deprecated alias (empty: %s)", (empty) => { diff --git a/src/__tests__/types/_program.ts b/src/__tests__/types/_program.ts new file mode 100644 index 00000000..45b3a2ec --- /dev/null +++ b/src/__tests__/types/_program.ts @@ -0,0 +1,67 @@ +import { join } from "node:path"; + +import ts from "typescript"; + +export const root = join(__dirname, "../../..").replaceAll("\\", "/"); + +// The repository's own compiler options with exactOptionalPropertyTypes on, and a fixture's unused names allowed +export function compilerOptions(): ts.CompilerOptions { + const { config } = ts.readConfigFile(`${root}/tsconfig.json`, (path) => + ts.sys.readFile(path), + ); + const { options } = ts.parseJsonConfigFileContent(config, ts.sys, root); + return { + ...options, + exactOptionalPropertyTypes: true, + noUnusedLocals: false, + noUnusedParameters: false, + }; +} + +export function createProgram( + rootNames: readonly string[], + virtualFiles: Readonly> = {}, +): ts.Program { + const options = compilerOptions(); + const host = ts.createCompilerHost(options, true); + + return ts.createProgram(rootNames, options, { + ...host, + fileExists: (name) => name in virtualFiles || host.fileExists(name), + readFile: (name) => virtualFiles[name] ?? host.readFile(name), + getSourceFile: (name, languageVersion, ...rest) => { + const text = virtualFiles[name]; + return text === undefined + ? host.getSourceFile(name, languageVersion, ...rest) + : ts.createSourceFile(name, text, languageVersion, true); + }, + }); +} + +export function createLanguageService( + virtualFiles: Readonly>, +): ts.LanguageService { + const options = compilerOptions(); + + return ts.createLanguageService( + { + getScriptFileNames: () => Object.keys(virtualFiles), + getScriptVersion: () => "1", + getScriptSnapshot: (name) => { + const text = virtualFiles[name] ?? ts.sys.readFile(name); + return text === undefined + ? undefined + : ts.ScriptSnapshot.fromString(text); + }, + getCurrentDirectory: () => root, + getCompilationSettings: () => options, + getDefaultLibFileName: ts.getDefaultLibFilePath, + fileExists: (name) => name in virtualFiles || ts.sys.fileExists(name), + readFile: (name) => virtualFiles[name] ?? ts.sys.readFile(name), + readDirectory: (...args) => ts.sys.readDirectory(...args), + directoryExists: (name) => ts.sys.directoryExists(name), + getDirectories: (name) => ts.sys.getDirectories(name), + }, + ts.createDocumentRegistry(), + ); +} diff --git a/src/__tests__/types/optional-members.test.ts b/src/__tests__/types/optional-members.test.ts new file mode 100644 index 00000000..eb8ed8fc --- /dev/null +++ b/src/__tests__/types/optional-members.test.ts @@ -0,0 +1,227 @@ +import ts from "typescript"; + +import { createProgram, root } from "./_program"; + +// Every declaration of a file, or only the option types a consumer writes +const declarations: readonly (readonly [string, readonly string[] | "all"])[] = + [ + ["types.d.ts", "all"], + ["src/runtime.types.ts", "all"], + [ + "src/compiler/compiler.types.ts", + ["CompilerOptions", "InlineVariableOptions"], + ], + ["src/metro/index.ts", ["WithReactNativeCSSOptions"]], + [ + "src/jest/index.ts", + ["RegisterCSSOptions", "CompileWithAutoDebugOptions"], + ], + ["src/babel/helpers.ts", ["PluginOpts"]], + ["src/native/react/useNativeCss.ts", ["Config"]], + ]; + +// Under exactOptionalPropertyTypes a bare `?: T` rejects an explicit undefined, which React Native's own optional props accept +function optionalMembersRejectingUndefined( + program: ts.Program, + fileName: string, + names: readonly string[] | "all" = "all", +): readonly string[] { + const checker = program.getTypeChecker(); + const source = program.getSourceFile(fileName); + if (source === undefined) throw new Error(`${fileName} is not compiled`); + + const rejectsUndefined = (type: ts.Type): boolean => + // `?: never` marks a key that must stay absent + (type.flags & ts.TypeFlags.Never) === 0 && + !checker.isTypeAssignableTo(checker.getUndefinedType(), type); + + const typeNodeRejectsUndefined = (node: ts.TypeNode | undefined) => + node !== undefined && rejectsUndefined(checker.getTypeFromTypeNode(node)); + + // keyof T through any alias, or a type parameter declared as one + const isKeyOfConstraint = (node: ts.TypeNode): boolean => { + if ((checker.getTypeFromTypeNode(node).flags & ts.TypeFlags.Index) !== 0) { + return true; + } + const declaration = ts.isTypeReferenceNode(node) + ? checker.getSymbolAtLocation(node.typeName)?.declarations?.[0] + : undefined; + return ( + declaration !== undefined && + ts.isTypeParameterDeclaration(declaration) && + declaration.constraint !== undefined && + isKeyOfConstraint(declaration.constraint) + ); + }; + + // A mapped type over keyof T keeps T's optional members optional unless it says -? + const isOptionalMappedType = (node: ts.MappedTypeNode): boolean => { + if (node.questionToken !== undefined) { + return node.questionToken.kind !== ts.SyntaxKind.MinusToken; + } + const constraint = node.typeParameter.constraint; + return constraint !== undefined && isKeyOfConstraint(constraint); + }; + + // { [K in keyof T]: T[K] } keeps each member's own type, undefined included + const isTransparentMappedType = (node: ts.MappedTypeNode): boolean => { + if (node.type === undefined || !ts.isIndexedAccessTypeNode(node.type)) { + return false; + } + const { objectType, indexType } = node.type; + return ( + ts.isTypeReferenceNode(indexType) && + ts.isIdentifier(indexType.typeName) && + indexType.typeName.text === node.typeParameter.name.text && + node.typeParameter.constraint !== undefined && + ts.isTypeOperatorNode(node.typeParameter.constraint) && + node.typeParameter.constraint.operator === ts.SyntaxKind.KeyOfKeyword && + node.typeParameter.constraint.type.getText(source) === + objectType.getText(source) + ); + }; + + // A library's mapped type that adds `?`, such as Partial, makes members its source file never spells out + const addsOptionalMembersRejectingUndefined = ( + node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, + ): boolean => { + const type = ts.isTypeReferenceNode(node) + ? checker.getTypeFromTypeNode(node) + : checker.getTypeAtLocation(node); + const declaration = type.aliasSymbol?.declarations?.[0]; + const argument = type.aliasTypeArguments?.[0]; + if ( + declaration === undefined || + argument === undefined || + declaration.getSourceFile() === source || + !ts.isTypeAliasDeclaration(declaration) || + !ts.isMappedTypeNode(declaration.type) || + declaration.type.questionToken === undefined || + declaration.type.questionToken.kind === ts.SyntaxKind.MinusToken + ) { + return false; + } + // Over a type parameter nothing proves its members accept undefined + return ( + argument.isTypeParameter() || + checker + .getPropertiesOfType(argument) + .some((property) => rejectsUndefined(checker.getTypeOfSymbol(property))) + ); + }; + + const offenders: string[] = []; + + const visit = (node: ts.Node): void => { + const rejects = + ((ts.isPropertySignature(node) || + ts.isPropertyDeclaration(node) || + ts.isNamedTupleMember(node)) && + node.questionToken !== undefined && + typeNodeRejectsUndefined(node.type)) || + ((ts.isMethodSignature(node) || ts.isMethodDeclaration(node)) && + node.questionToken !== undefined) || + (ts.isOptionalTypeNode(node) && typeNodeRejectsUndefined(node.type)) || + (ts.isMappedTypeNode(node) && + isOptionalMappedType(node) && + !isTransparentMappedType(node) && + typeNodeRejectsUndefined(node.type)) || + ((ts.isTypeReferenceNode(node) || + ts.isExpressionWithTypeArguments(node)) && + addsOptionalMembersRejectingUndefined(node)); + + if (rejects) { + const { line } = source.getLineAndCharacterOfPosition( + node.getStart(source), + ); + offenders.push(`${line + 1}: ${node.getText(source).split("\n")[0]}`); + } + + ts.forEachChild(node, visit); + }; + + if (names === "all") { + visit(source); + } else { + for (const statement of source.statements) { + if ( + (ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement)) && + names.includes(statement.name.text) + ) { + visit(statement); + } + } + } + + return offenders; +} + +test("the walk finds each form of an optional member that rejects undefined, and leaves a never member", () => { + const fileName = `${root}/src/__tests__/types/optional-members.fixture.ts`; + const program = createProgram([fileName], { + [fileName]: `type Maybe = T | undefined; +interface Source { + value?: string; +} +export interface Props { + bare?: string; + widened?: string | undefined; + absent?: never; + method?(): void; + aliased?: Maybe; + voided?: void; + parenthesised?: (string | undefined); + indexed?: Source["value"]; +} +export declare class Options { + passThrough?: boolean; + widened?: boolean | undefined; +} +export type Inherited = { [K in keyof T]: string }; +export type Stripped = { [K in keyof T]-?: string }; +export type Optional = { [K in keyof T]?: string | undefined }; +export type Picked = { [Q in K]: string }; +export type Partly = Partial<{ passThrough: boolean }>; +export type PartlyWidened = Partial<{ passThrough: boolean | undefined }>; +export type Pair = [string, number?]; +export type Named = [first: string, second?: number | undefined]; +interface Base { passThrough: boolean } +export interface ExtendsPartial extends Partial {} +export type GenericPartial = Partial; +type KeysOf = keyof T; +export type ViaAlias = { [K in KeysOf]: string }; +export type Same = { [K in keyof T]: T[K] }; +export type Mutable = { -readonly [K in keyof T]: T[K] }; +`, + }); + + expect(optionalMembersRejectingUndefined(program, fileName)).toStrictEqual([ + "3: value?: string;", + "6: bare?: string;", + "9: method?(): void;", + "16: passThrough?: boolean;", + "19: { [K in keyof T]: string }", + "22: { [Q in K]: string }", + "23: Partial<{ passThrough: boolean }>", + "25: number?", + "28: Partial", + "29: Partial", + "31: { [K in KeysOf]: string }", + ]); +}); + +test("every optional member a consumer writes accepts an explicit undefined, or is never and must stay absent", () => { + const program = createProgram( + declarations.map(([file]) => `${root}/${file}`), + ); + + expect( + Object.fromEntries( + declarations.map(([file, names]) => [ + file, + optionalMembersRejectingUndefined(program, `${root}/${file}`, names), + ]), + ), + ).toStrictEqual(Object.fromEntries(declarations.map(([file]) => [file, []]))); +}); diff --git a/src/__tests__/types/styled.test.ts b/src/__tests__/types/styled.test.ts new file mode 100644 index 00000000..733916e7 --- /dev/null +++ b/src/__tests__/types/styled.test.ts @@ -0,0 +1,96 @@ +import ts from "typescript"; + +import { createLanguageService, createProgram, root } from "./_program"; + +const fixtureName = `${root}/src/__tests__/types/styled.fixture.tsx`; + +const prelude = [ + `import type { ComponentProps } from "react";`, + `import { ActivityIndicator, Image, Text, TextInput, View } from "react-native";`, + `import { styled } from "react-native-css";`, + `const StyledView = styled(View);`, + `const MappedText = styled(Text, { className: { target: "style" } });`, +] as const; + +function compiles(cases: Readonly>) { + const names = Object.keys(cases); + const text = [ + ...prelude, + ...names.map((name) => `export const ${name} = ${cases[name]};`), + ].join("\n"); + const program = createProgram([fixtureName], { [fixtureName]: text }); + const source = program.getSourceFile(fixtureName); + if (source === undefined) throw new Error("the fixture is not compiled"); + + const failing = new Set( + ts.getPreEmitDiagnostics(program, source).map((diagnostic) => { + const { line } = source.getLineAndCharacterOfPosition( + diagnostic.start ?? 0, + ); + return names[line - prelude.length] ?? `prelude line ${line + 1}`; + }), + ); + return Object.fromEntries( + [...names, ...failing] + .filter((name, index, all) => all.indexOf(name) === index) + .map((name) => [name, !failing.has(name)]), + ); +} + +test("a mapping takes any style key, a -rn- declaration's among them, and a value naming a prop", () => { + expect( + compiles({ + colorOfView: `styled(View, { className: { target: "style", nativeStyleMapping: { color: "accessibilityLabel" } } })`, + colorOfActivityIndicator: `styled(ActivityIndicator, { className: { target: "style", nativeStyleMapping: { color: "color" } } })`, + rnDeclaration: `styled(TextInput, { className: { target: "style", nativeStyleMapping: { selectionColor: true } } })`, + keptName: `styled(Image, { className: { target: "style", nativeStyleMapping: { tintColor: true } } })`, + leftUndefined: `styled(View, { className: { target: "style", nativeStyleMapping: { backgroundColor: undefined, fill: undefined } } })`, + intoStyleFromTargetFalse: `styled(View, { labelClassName: { target: false, nativeStyleMapping: { color: "style.borderColor" } } })`, + deprecatedAlias: `styled(Text, { className: { target: "style", nativeStyleToProp: { color: "accessibilityLabel" } } })`, + noSuchProp: `styled(View, { className: { target: "style", nativeStyleMapping: { color: "accessibilityLabl" } } })`, + noSuchPropFromTargetFalse: `styled(View, { labelClassName: { target: false, nativeStyleMapping: { color: "style.borderColour" } } })`, + falseValue: `styled(View, { className: { target: "style", nativeStyleMapping: { color: false } } })`, + }), + ).toStrictEqual({ + colorOfView: true, + colorOfActivityIndicator: true, + rnDeclaration: true, + keptName: true, + leftUndefined: true, + intoStyleFromTargetFalse: true, + deprecatedAlias: true, + noSuchProp: false, + noSuchPropFromTargetFalse: false, + falseValue: false, + }); +}); + +test("a mapping completes React Native's style keys, whatever the target", () => { + const text = [ + ...prelude, + `export const mapped = styled(View, { labelClassName: { target: false, nativeStyleMapping: { /*keys*/ } } });`, + ].join("\n"); + const completions = createLanguageService({ + [fixtureName]: text, + }).getCompletionsAtPosition(fixtureName, text.indexOf("/*keys*/"), {}); + + expect( + completions?.entries + .map(({ name }) => name) + .filter((name) => + ["backgroundColor", "color", "tintColor", "fill", "stroke"].includes( + name, + ), + ) + .sort(), + ).toStrictEqual(["backgroundColor", "color", "fill", "stroke", "tintColor"]); +}); + +test("a styled component's class props are writable, as the component's own props are", () => { + expect( + compiles({ + defaultConfiguration: `(props: ComponentProps) => { props.className = "x"; }`, + ownConfiguration: `(props: ComponentProps) => { props.className = "x"; }`, + }), + ).toStrictEqual({ defaultConfiguration: true, ownConfiguration: true }); +}); diff --git a/src/babel/helpers.ts b/src/babel/helpers.ts index c66f630c..8b045e58 100644 --- a/src/babel/helpers.ts +++ b/src/babel/helpers.ts @@ -3,9 +3,9 @@ import tBabelTypes, { type CallExpression } from "@babel/types"; export type BabelTypes = typeof tBabelTypes; export interface PluginOpts { - target?: string; - runtime?: string; - commonjs?: boolean; + target?: string | undefined; + runtime?: string | undefined; + commonjs?: boolean | undefined; } export interface PluginState { diff --git a/src/compiler/compiler.types.ts b/src/compiler/compiler.types.ts index 00e08785..6d43b6a7 100644 --- a/src/compiler/compiler.types.ts +++ b/src/compiler/compiler.types.ts @@ -8,20 +8,20 @@ import type { import { VAR_SYMBOL } from "../native/reactivity"; export interface CompilerOptions { - filename?: string; - projectRoot?: string; - inlineRem?: number | false; - inlineVariables?: false | InlineVariableOptions; - selectorPrefix?: string; - stylesheetOrder?: number; - features?: FeatureFlagRecord; - logger?: (message: string) => void | Debugger; - hexColors?: boolean; - colorPrecision?: number; + filename?: string | undefined; + projectRoot?: string | undefined; + inlineRem?: number | false | undefined; + inlineVariables?: false | InlineVariableOptions | undefined; + selectorPrefix?: string | undefined; + stylesheetOrder?: number | undefined; + features?: FeatureFlagRecord | undefined; + logger?: ((message: string) => void | Debugger) | undefined; + hexColors?: boolean | undefined; + colorPrecision?: number | undefined; } export interface InlineVariableOptions { - exclude?: `--${string}`[]; + exclude?: `--${string}`[] | undefined; } /** diff --git a/src/jest/index.ts b/src/jest/index.ts index a8d249f5..a76a920e 100644 --- a/src/jest/index.ts +++ b/src/jest/index.ts @@ -31,10 +31,15 @@ const debugDefault = Boolean( process.env.NODE_OPTIONS.includes("--inspect"), ); -export function registerCSS( - css: string, - options: CompilerOptions & { debug?: boolean } = {}, -) { +type RegisterCSSOptions = CompilerOptions & { + debug?: boolean | undefined; +}; + +type CompileWithAutoDebugOptions = CompilerOptions & { + debug?: boolean | "verbose" | undefined; +}; + +export function registerCSS(css: string, options: RegisterCSSOptions = {}) { const { debug = debugDefault } = options; const compiled = compileWithAutoDebug(css, options); @@ -57,10 +62,7 @@ export function registerCSS( export function compileWithAutoDebug( css: string, - { - debug = debugDefault, - ...options - }: CompilerOptions & { debug?: boolean | "verbose" } = {}, + { debug = debugDefault, ...options }: CompileWithAutoDebugOptions = {}, ) { const logger = debug ? (text: string) => { diff --git a/src/metro/index.ts b/src/metro/index.ts index 63290e59..9203bdcd 100644 --- a/src/metro/index.ts +++ b/src/metro/index.ts @@ -10,12 +10,12 @@ import { setupTypeScript } from "./typescript"; export interface WithReactNativeCSSOptions extends CompilerOptions { /* Specify the path to the TypeScript environment file. Defaults types-env.d.ts */ - typescriptEnvPath?: string; + typescriptEnvPath?: string | undefined; /* Disable generation of the types-env.d.ts file. Defaults false */ - disableTypeScriptGeneration?: boolean; + disableTypeScriptGeneration?: boolean | undefined; /** Add className to all React Native primitives. Defaults false */ - globalClassNamePolyfill?: boolean; - hexColors?: boolean; + globalClassNamePolyfill?: boolean | undefined; + hexColors?: boolean | undefined; } const metroOverrideResolution = { diff --git a/src/native/api.tsx b/src/native/api.tsx index 6736f746..475ff741 100644 --- a/src/native/api.tsx +++ b/src/native/api.tsx @@ -1,5 +1,5 @@ /* eslint-disable */ -import { useContext, useEffect, useState, type ComponentType } from "react"; +import { useContext, useEffect, useState } from "react"; import { Appearance, type ViewStyle } from "react-native"; import type { StyleDescriptor } from "react-native-css/compiler"; @@ -7,6 +7,7 @@ import { VariableContext } from "react-native-css/native-internal"; import type { ColorScheme, + DefaultStyledConfiguration, Props, ReactComponent, Styled, @@ -32,9 +33,9 @@ export { export { useNativeCss }; -const defaultMapping: StyledConfiguration> = { +const defaultMapping = { className: "style", -}; +} as const satisfies DefaultStyledConfiguration; /** * Generates a new Higher-Order component the wraps the base component and applies the styles. diff --git a/src/native/react/useNativeCss.ts b/src/native/react/useNativeCss.ts index f38c77c2..aba77b1b 100644 --- a/src/native/react/useNativeCss.ts +++ b/src/native/react/useNativeCss.ts @@ -28,7 +28,7 @@ import { updateRules } from "./rules"; export type Config = { source: string; target: string[] | string | false; - nativeStyleMapping?: Record; + nativeStyleMapping?: Record | undefined; }; export type ComponentState = { @@ -210,7 +210,11 @@ export function mappingToConfig(mapping: StyledConfiguration) { const mapping = value.nativeStyleMapping ?? value.nativeStyleToProp; const nativeStyleMapping = mapping ? Object.fromEntries( - Object.entries(mapping).map(([k, v]) => [k, v === true ? k : v]), + Object.entries(mapping).flatMap(([property, path]) => + path === undefined + ? [] + : [[property, path === true ? property : path] as const], + ), ) : undefined; diff --git a/src/runtime.types.ts b/src/runtime.types.ts index 69cab6ca..e7663acf 100644 --- a/src/runtime.types.ts +++ b/src/runtime.types.ts @@ -16,41 +16,36 @@ import type { ViewStyle, } from "react-native"; -import type { DotNotation, ResolveDotPath } from "react-native-css/utilities"; +import type { DotNotation } from "react-native-css/utilities"; /******************************** API ********************************/ -export type StyledReactElement< - C extends ReactComponent, - M extends StyledConfiguration, -> = ReactElement< - ComponentProps & { - [K in keyof M as K extends string - ? M[K] extends undefined | false - ? never - : M[K] extends true | string | object - ? K - : never - : never]?: string; - } ->; - -export type StyledProps> = P & { - [K in keyof M as K extends string +type StyledSourceProps = { + -readonly [K in keyof M as K extends string ? M[K] extends undefined | false ? never : M[K] extends true | string | object ? K : never - : never]?: string; + : never]?: string | undefined; }; +export type StyledReactElement< + C extends ReactComponent, + M extends StyledConfiguration, +> = ReactElement & StyledSourceProps>; + +export type StyledProps> = P & + StyledSourceProps; + export interface Styled { ( component: C, mapping?: undefined, options?: StyledOptions, - ): ComponentType & { className?: string }>; + ): ComponentType< + ComponentPropsWithRef & StyledSourceProps + >; >( component: C, mapping: M & StyledConfiguration, @@ -61,17 +56,11 @@ export interface Styled { type StyledComponent< C extends ReactComponent, M extends StyledConfiguration, -> = ComponentType< - ComponentPropsWithRef & { - [K in keyof M as K extends string - ? M[K] extends undefined | false - ? never - : M[K] extends true | string | object - ? K - : never - : never]?: string; - } ->; +> = ComponentType & StyledSourceProps>; + +export interface DefaultStyledConfiguration { + readonly className: "style"; +} export type StyledConfiguration< C extends ReactComponent, @@ -88,27 +77,21 @@ interface StyledConfigurationObject< T extends ComponentPropsDotNotation | false, > { target: T; - nativeStyleMapping?: T extends false - ? NativeStyleMapping> - : NativeStyleMapping< - ResolveDotPath>, - ComponentProps - >; + nativeStyleMapping?: NativeStyleMapping> | undefined; /** @deprecated Please use nativeStyleMapping */ nativeStyleToProp?: StyledConfigurationObject["nativeStyleMapping"]; } -type NativeStyleMapping = T extends object - ? { - [K in keyof T as K extends string ? K : never]: true | DotNotation; - } & { - fill?: true | DotNotation; - stroke?: true | DotNotation; - } - : Record>; +// A -rn- declaration puts its camel-cased name in the style, so a key may be any string beside React Native's style keys +type NativeStyleMapping = Readonly< + Partial>> +> & + Readonly>>; + +type NativeStyleMappingValue = true | DotNotation | undefined; export interface StyledOptions { - passThrough?: boolean; + passThrough?: boolean | undefined; } /*************************** React Helpers ***************************/ diff --git a/src/web/api.tsx b/src/web/api.tsx index 28e09ab8..7e0aea9e 100644 --- a/src/web/api.tsx +++ b/src/web/api.tsx @@ -1,9 +1,4 @@ -import { - createElement, - useMemo, - type ComponentType, - type PropsWithChildren, -} from "react"; +import { createElement, useMemo, type PropsWithChildren } from "react"; import { Appearance } from "react-native"; import type { @@ -13,12 +8,16 @@ import type { StyledOptions, } from "react-native-css"; -import type { ReactComponent, Styled } from "../runtime.types"; +import type { + DefaultStyledConfiguration, + ReactComponent, + Styled, +} from "../runtime.types"; import { assignStyle } from "./assign-style"; -const defaultMapping: StyledConfiguration> = { +const defaultMapping = { className: "style", -}; +} as const satisfies DefaultStyledConfiguration; export const styled: Styled = ( baseComponent: ReactComponent, diff --git a/types.d.ts b/types.d.ts index 924b9a7d..9811ec12 100644 --- a/types.d.ts +++ b/types.d.ts @@ -10,44 +10,44 @@ 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; + contentContainerClassName?: string | undefined; } interface FlatListProps extends VirtualizedListProps { - columnWrapperClassName?: string; + columnWrapperClassName?: string | undefined; } interface ImageBackgroundProps extends ImagePropsBase { - imageClassName?: string; + imageClassName?: string | undefined; } interface ImagePropsBase { - className?: string; + className?: string | undefined; } interface ViewProps { - className?: string; + className?: string | undefined; } interface TextProps { - className?: string; + className?: string | undefined; } interface SwitchProps { - className?: string; + className?: string | undefined; } interface TouchableWithoutFeedbackProps { - className?: string; + className?: string | undefined; } interface KeyboardAvoidingViewProps extends ViewProps { - contentContainerClassName?: string; + contentContainerClassName?: string | undefined; } }