React Native forms powered by tcomb-validation. Modern TypeScript build with legacy-compatible API (drop-in replacement).
npm install @riebel/tcomb-form-native-ts


A modern TypeScript implementation of tcomb-form-native with React 18+ support, functional components, and 100% API compatibility with the original library.
- Setup
- Supported React Native Versions
- Example
- API
- Types
- Rendering Options
- Unions
- Lists
- Customizations
- Migration Guide
- Tests
- License
``bash`
npm install @riebel/tcomb-form-native-ts
`bash`
npx expo install @react-native-picker/picker
`bash`
npm install @react-native-picker/picker
npx react-native link @react-native-picker/picker
| Version | React Native Support | TypeScript | React |
|---------|---------------------|------------|-------|
| 1.1.x | 0.60.0+ | 4.5+ | 18.0+ |
This library uses modern React patterns including hooks and functional components.
The tcomb library provides a concise but expressive way to define domain models in JavaScript/TypeScript.
The tcomb-validation library builds on tcomb, providing validation functions for tcomb domain models.
This library builds on those two and React Native, providing a modern TypeScript implementation with full backward compatibility.
With tcomb-form-native you simply call
to generate a form based on that domain model. What does this get you?1. Write a lot less code
2. Usability and accessibility for free (automatic labels, inline validation, etc)
3. No need to update forms when domain model changes
4. Full TypeScript support with type safety
5. Modern React patterns (hooks, functional components)
$3
JSON Schemas are also supported via the tcomb-json-schema library.
Note: Please use tcomb-json-schema ^0.2.5.
$3
The look and feel is customizable via React Native stylesheets and templates (see documentation).
Example
`tsx
// App.tsx
import React, { useRef, useState } from 'react';
import { View, Text, TouchableHighlight, StyleSheet, Alert } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';const Form = t.form.Form;
// Define your domain model
const Person = t.struct({
name: t.String, // a required string
surname: t.maybe(t.String), // an optional string
age: t.Number, // a required number
rememberMe: t.Boolean // a boolean
});
const options = {}; // optional rendering options (see documentation)
export default function App() {
const formRef = useRef(null);
const handlePress = () => {
// call getValue() to get the values of the form
const value = formRef.current?.getValue();
if (value) { // if validation fails, value will be null
console.log(value); // value here is an instance of Person
Alert.alert('Success',
Hello ${value.name}!);
}
}; return (
ref={formRef}
type={Person}
options={options}
/>
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
Save
);
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
button: {
height: 36,
backgroundColor: '#48BBEC',
borderColor: '#48BBEC',
borderWidth: 1,
borderRadius: 8,
marginBottom: 10,
alignSelf: 'stretch',
justifyContent: 'center'
}
});
`Output:
(Labels are automatically generated)
Output after a validation error:
The form will highlight validation errors automatically.
API
$3
Returns
null if the validation failed, an instance of your model otherwise.> Note: Calling
getValue will cause the validation of all the fields of the form, including some side effects like highlighting the errors.$3
Returns a
ValidationResult (see tcomb-validation for reference documentation).$3
The
Form component behaves like a controlled component:`tsx
import React, { useRef, useState } from 'react';
import { View, Text, TouchableHighlight } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';const Person = t.struct({
name: t.String,
surname: t.maybe(t.String)
});
export default function App() {
const formRef = useRef(null);
const [value, setValue] = useState({
name: 'Giulio',
surname: 'Canti'
});
const handleChange = (newValue: unknown) => {
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
ref={formRef}
type={Person}
value={value}
onChange={handleChange}
/>
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
Save
);
}
`The
onChange handler has the following signature:`typescript
(raw: unknown, path: Array) => void
`where:
-
raw contains the current raw value of the form (can be an invalid value for your model)
- path is the path to the field triggering the change$3
`tsx
import React, { useRef, useState } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';const Type = t.struct({
disable: t.Boolean, // if true, name field will be disabled
name: t.String
});
export default function App() {
const formRef = useRef(null);
const [value, setValue] = useState(null);
const [options, setOptions] = useState({
fields: {
name: {}
}
});
const handleChange = (newValue: unknown) => {
// Update options based on form value
const newOptions = {
...options,
fields: {
...options.fields,
name: {
...options.fields.name,
editable: !newValue?.disable
}
}
};
setOptions(newOptions);
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
ref={formRef}
type={Type}
options={options}
value={value}
onChange={handleChange}
/>
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
Save
);
}
`$3
`tsx
import React, { useRef, useState } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';const Person = t.struct({
name: t.String,
surname: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean
});
export default function App() {
const formRef = useRef(null);
const [value, setValue] = useState(null);
const handleChange = (newValue: unknown) => {
setValue(newValue);
};
const clearForm = () => {
// clear content from all fields
setValue(null);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
// clear all fields after submit
clearForm();
}
};
return (
ref={formRef}
type={Person}
value={value}
onChange={handleChange}
/>
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
Save
);
}
`$3
`tsx
import React, { useRef, useState, useMemo } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';const Country = t.enums({
'IT': 'Italy',
'US': 'United States'
}, 'Country');
export default function App() {
const formRef = useRef(null);
const [value, setValue] = useState>({});
// Returns the suitable type based on the form value
const getType = (formValue: Record) => {
if (formValue.country === 'IT') {
return t.struct({
country: Country,
rememberMe: t.Boolean
});
} else if (formValue.country === 'US') {
return t.struct({
country: Country,
name: t.String
});
} else {
return t.struct({
country: Country
});
}
};
const type = useMemo(() => getType(value), [value]);
const handleChange = (newValue: Record) => {
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
ref={formRef}
type={type}
value={value}
onChange={handleChange}
/>
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
Save
);
}
`Types
$3
By default fields are required:
`typescript
const Person = t.struct({
name: t.String, // a required string
surname: t.String // a required string
});
`$3
In order to create an optional field, wrap the field type with the
t.maybe combinator:`typescript
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String) // an optional string
});
`The postfix
" (optional)" is automatically added to optional fields.You can customize the postfix value or set a postfix for required fields:
`typescript
t.form.Form.i18n = {
optional: '',
required: ' (required)' // inverting the behavior: adding a postfix to required fields
};
`$3
In order to create a numeric field, use the
t.Number type:`typescript
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number // a numeric field
});
`tcomb-form-native will convert automatically numbers to/from strings.
$3
In order to create a boolean field, use the
t.Boolean type:`typescript
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean // a boolean field
});
`Booleans are displayed as checkboxes.
$3
In order to create a date field, use the
t.Date type:`typescript
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
birthDate: t.Date // a date field
});
`Dates are displayed as date pickers on both iOS and Android.
$3
In order to create an enum field, use the
t.enums combinator:`typescript
const Gender = t.enums({
M: 'Male',
F: 'Female'
});const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean,
gender: Gender // enum
});
`Enums are displayed as
Picker components.$3
A predicate is a function with the following signature:
`typescript
(x: unknown) => boolean
`You can refine a type with the
t.refinement(type, predicate) combinator:`typescript
// a type representing positive numbers
const Positive = t.refinement(t.Number, (n: number) => {
return n >= 0;
});const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: Positive, // refinement
rememberMe: t.Boolean,
gender: Gender
});
`Subtypes allow you to express custom validation with a simple predicate.
Rendering Options
In order to customize the look and feel, use an
options prop:`tsx
`$3
#### Labels and Placeholders
By default labels are automatically generated. You can turn off this behavior or override the default labels on a field basis.
`typescript
const options = {
label: 'My struct label' // <= form legend, displayed before the fields
};const options = {
fields: {
name: {
label: 'My name label' // <= label for the name field
}
}
};
`In order to automatically generate default placeholders, use the option
auto: 'placeholders':`typescript
const options = {
auto: 'placeholders'
};
`Set
auto: 'none' if you don't want neither labels nor placeholders.`typescript
const options = {
auto: 'none'
};
`#### Fields Order
You can sort the fields with the
order option:`typescript
const options = {
order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email']
};
`#### Default Values
You can set the default values by passing a
value prop to the Form component:`typescript
const value = {
name: 'Giulio',
surname: 'Canti',
age: 41,
gender: 'M'
};
`#### Fields Configuration
You can configure each field with the
fields option:`typescript
const options = {
fields: {
name: {
// name field configuration here..
},
surname: {
// surname field configuration here..
}
}
};
`$3
Implementation:
TextInputTech note: Values containing only white spaces are converted to
null.#### Placeholder
`typescript
const options = {
fields: {
name: {
placeholder: 'Your placeholder here'
}
}
};
`#### Label
`typescript
const options = {
fields: {
name: {
label: 'Insert your name'
}
}
};
`#### Help Message
`typescript
const options = {
fields: {
name: {
help: 'Your help message here'
}
}
};
`#### Error Messages
`typescript
const options = {
fields: {
email: {
error: 'Insert a valid email'
}
}
};
`error can also be a function:`typescript
(value: unknown, path: string[], context: Record) => string | null
`#### Standard TextInput Options
The following standard options are available:
-
allowFontScaling
- autoCapitalize
- autoCorrect
- autoFocus
- clearButtonMode
- editable
- enablesReturnKeyAutomatically
- keyboardType
- maxLength
- multiline
- onBlur
- onEndEditing
- onFocus
- onSubmitEditing
- onContentSizeChange
- placeholderTextColor
- returnKeyType
- selectTextOnFocus
- secureTextEntry
- selectionState
- textAlign
- textAlignVertical$3
Implementation:
Picker from @react-native-picker/picker#### nullOption
`typescript
const options = {
fields: {
gender: {
nullOption: {value: '', text: 'Choose your gender'}
}
}
};
`You can remove the null option by setting
nullOption to false.#### Options Order
`typescript
const options = {
fields: {
gender: {
order: 'asc' // or 'desc'
}
}
};
`$3
Implementation: Platform-specific date picker
`typescript
const Person = t.struct({
name: t.String,
birthDate: t.Date
});
`$3
For every component, you can set the field with the
hidden option:`typescript
const options = {
fields: {
name: {
hidden: true
}
}
};
`Lists
You can handle a list with the
t.list combinator:`typescript
const Person = t.struct({
name: t.String,
tags: t.list(t.String) // a list of strings
});
`$3
To configure all the items in a list, set the
item option:`typescript
const Person = t.struct({
name: t.String,
tags: t.list(t.String)
});const options = {
fields: {
tags: {
item: {
label: 'My tag'
}
}
}
};
`$3
`typescript
const Person = t.struct({
name: t.String,
surname: t.String
});const Persons = t.list(Person);
`$3
`typescript
const options = {
i18n: {
optional: ' (optional)',
required: '',
add: 'Add', // add button
remove: '✘', // remove button
up: '↑', // move up button
down: '↓' // move down button
}
};
`$3
`typescript
const options = {
disableAdd: false, // prevents adding new items
disableRemove: false, // prevents removing existing items
disableOrder: false // prevents sorting existing items
};
`Unions
`typescript
const AccountType = t.enums({
'type1': 'Type 1',
'type2': 'Type 2',
'other': 'Other'
}, 'AccountType');const KnownAccount = t.struct({
type: AccountType
}, 'KnownAccount');
const UnknownAccount = KnownAccount.extend({
label: t.String,
}, 'UnknownAccount');
const Account = t.union([KnownAccount, UnknownAccount], 'Account');
// Dispatch function to select the correct type
Account.dispatch = (value: Record) =>
value && value.type === 'other' ? UnknownAccount : KnownAccount;
const Type = t.list(Account);
const options = {
item: [
{ label: 'KnownAccount' },
{ label: 'UnknownAccount' }
]
};
`Customizations
$3
You can customize the look and feel by setting a custom stylesheet:
`typescript
import t from '@riebel/tcomb-form-native-ts';// Define a custom stylesheet
const customStylesheet = {
// ... your styles here
};
// Override globally
t.form.Form.stylesheet = customStylesheet;
`You can also override the stylesheet locally:
`typescript
const options = {
stylesheet: myCustomStylesheet
};
`Or per field:
`typescript
const options = {
fields: {
name: {
stylesheet: myCustomStylesheet
}
}
};
`$3
You can customize the layout by setting custom templates:
`typescript
import t from '@riebel/tcomb-form-native-ts';const customTemplates = {
// ... your templates here
};
// Override globally
t.form.Form.templates = customTemplates;
`Local template override:
`typescript
function myCustomTemplate(locals: Record) {
return (
{locals.label}
);
}const options = {
fields: {
name: {
template: myCustomTemplate
}
}
};
`$3
Transformers handle serialization/deserialization of data:
`typescript
interface Transformer {
format: (value: unknown) => unknown; // from value to string
parse: (value: unknown) => unknown; // from string to value
}
`Example for a search field that accepts space-separated keywords:
`typescript
const listTransformer = {
format: (value: string[]) => {
return Array.isArray(value) ? value.join(' ') : value;
},
parse: (str: string) => {
return str ? str.split(' ') : [];
}
};const options = {
fields: {
search: {
factory: t.form.Textbox,
transformer: listTransformer,
help: 'Keywords are separated by spaces'
}
}
};
`Migration from tcomb-form-native
This package is a 100% drop-in replacement for the original
tcomb-form-native. No code changes are required!$3
`bash
npm uninstall tcomb-form-native
or
yarn remove tcomb-form-native
`$3
`bash
npm install @riebel/tcomb-form-native-ts
or
yarn add @riebel/tcomb-form-native-ts
`$3
For the smoothest migration with absolutely no code changes, use npm package aliasing in your
package.json:`json
{
"dependencies": {
"tcomb-form-native": "npm:@riebel/tcomb-form-native-ts@^1.1.8"
}
}
`That's it! Now all your existing imports work without any changes:
`javascript
// This continues to work exactly the same - no code changes needed!
import t from 'tcomb-form-native';
const Form = t.form.Form;
`The
npm: prefix tells npm/yarn to install @riebel/tcomb-form-native-ts but make it available as tcomb-form-native in your project.$3
If you prefer not to use aliases, you can update your imports:
`javascript
// Updated import
import t from '@riebel/tcomb-form-native-ts';
const Form = t.form.Form;
`$3
Make sure you have the required peer dependencies:
`bash
For Expo projects
npx expo install @react-native-picker/pickerFor bare React Native projects
npm install @react-native-picker/picker
npx react-native link @react-native-picker/picker
`$3
Your existing code will work without any changes. The new package provides:
- ✅ 100% API compatibility - All existing code works unchanged
- ✅ Modern TypeScript support - Full type safety and IntelliSense
- ✅ React 18+ compatibility - Uses modern React patterns internally
- ✅ Functional components - Modernized implementation under the hood
- ✅ Better performance - Optimized with React.memo and hooks
- ✅ Active maintenance - Regular updates and bug fixes
$3
While not required, you can gradually modernize your existing code:
- Convert class components to functional components with hooks
- Add TypeScript type annotations
- Use modern JavaScript syntax (
const/let instead of var)
- Take advantage of improved TypeScript IntelliSenseTests
`bash
npm run test
``This TypeScript modernization is maintained by Hagen Sommerkorn (@riebel).
Original tcomb-form-native library created by Giulio Canti (@gcanti).
Special thanks to all the contributors who made the original library possible.