TypeScript usage guide
Important: public TypeScript examples prefer schema-dsl/pure + s so that schema authoring has no automatic String prototype side effect.
Read this early when choosing between pure DSL strings, DSL seed builders, namespace factories, and optional String extension ergonomics. It explains what TypeScript can infer, what remains runtime-only, and where editor hints are intentionally limited.
1. Quick start
1.1 Installation
1.2 Basic usage
import { s, validate } from 'schema-dsl/pure';
//define Schema
const userSchema = s({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-100'
});
//Verify data
const result = validate(userSchema, {
username: 'testuser',
email: 'test@example.com',
age: 25
});
if (result.valid) {
console.log('Validation passed:', result.data);
} else {
console.log('Validation failed:', result.errors);
}
2. Chained calls in TypeScript
2.1 Default no-global String types
By default, TypeScript does not receive global interface String chain declarations. This keeps native String methods such as trim() and toLowerCase() stable for projects that import schema-dsl/pure.
Direct string chaining therefore reports a type error unless you explicitly opt in to schema-dsl/string-types:
// ❌ TypeScript error by default
const defaultErrorSchema = s({
email: 'email!'.label('email') // Type error: Property 'label' does not exist on type 'string'
});
// ✅ Default TypeScript path without global String types: use a factory
const defaultBuilderSchema = s({
email: s.email().label('mailbox').require()
});
2.2 Recommended authoring entries
Choose one of the three recommended entries according to the authoring goal:
import { s } from 'schema-dsl/pure';
// ✅ Pure DSL: shortest configuration, limited editor hints inside the literal
const compactSchema = s({
email: 'email!',
username: 'string:3-32!'
});
// ✅ Explicit DSL seed: compact DSL plus builder hints
const emailField = s('email!').label('mailbox').pattern(/custom/);
const reusableSchema = s({ email: emailField });
// ✅ Factory form: strongest TypeScript method discovery
const accountEmail = s.email().label('mailbox').pattern(/custom/).require();
const factorySchema = s({ email: accountEmail });
The compatibility dsl export remains available, but new public examples use s as the short namespace. When imported from schema-dsl/pure, s supports pure DSL strings, s('...'), and s.xxx() without installing String extensions.
If the project uses transformSchemaDsl() or schemaDslEsbuildPlugin() to compile static String chains into builder calls, add the opt-in type entry:
import { s } from 'schema-dsl/pure';
import 'schema-dsl/string-types';
const schema = s({
role: 'admin|user|guest'.label('Role'),
email: 'email!'.label('Email').require()
});
Direct String chains are intentionally not the default path. Use them when the compact source form matters and your project explicitly installs the compile-time or runtime String extension support.
benefit:
- ✅ Pure DSL strings remain the shortest schema configuration.
- ✅
s('...') keeps DSL syntax while adding complete builder-method hints after the seed.
- ✅
s.email() / s.string() / s.number() give the strongest factory and method discovery.
- ✅ The
schema-dsl/pure entry does not install String extensions.
- ✅ The transform +
schema-dsl/string-types path gives String-chain hints only when explicitly requested.
- ✅ Known DSL literals still get lightweight value inference through
InferSchema / InferDslString.
2.3 Working principle
// s factory and s(string) both return a builder typed with the public IDslBuilder chain contract
const emailBuilder = s.email().require();
const sameBuilderShape = s('email!').label('mailbox');
// ^? IDslBuilder - complete public chain type
// DslBuilder supports all chained methods and has complete type hints
emailBuilder.label('mailbox')
// ^? IDE automatically prompts all available methods
.pattern(/^[a-z]+@[a-z]+\.[a-z]+$/)
.error({ required: 'Email required' });
s('string:3-32!') should not be described as a full type-level parser for the DSL grammar. It gives full type hints for the returned builder methods, while the string literal itself is only inferred at a coarse schema-value level by helper types such as InferDslString<'string:3-32!'> -> string. Length ranges, regular expressions, custom validators, and localized messages are runtime schema constraints, not TypeScript value refinements.
3. Best practices for type inference
3.1 Comparison of methods
3.2 Recommended writing method
✅ Way 1: Pure DSL strings for the shortest configuration
import { s } from 'schema-dsl/pure';
const schema = s({
username: 'username:medium!',
email: 'email!',
age: 'number:18-100'
});
advantage:
- ✅ Shortest public authoring form
- ✅ No String extension installation
- ✅ Best for fields that only need built-in DSL constraints
✅ Way 2: Define DSL seeds first and then combine them
import { s } from 'schema-dsl/pure';
//Define reusable fields
const emailField = s('email!')
.label('email address')
.error({ required: 'Email required' });
const usernameField = s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('username')
.error({ pattern: 'Username can only contain letters, numbers and underscores' });
// Use in combination
const registrationSchema = s({
email: emailField,
username: usernameField,
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('password')
.error({ pattern: 'Password must be at least 8 characters and must contain letters and numbers' })
});
const loginSchema = s({
email: emailField, //reuse
password: s('string!').label('password')
});
advantage:
- ✅ Field definitions can be reused
- ✅ The code is more modular
- ✅ Suitable for large projects
✅ Way 3: Use s namespace factories when discovery matters most
import { s } from 'schema-dsl/pure';
const schema = s({
username: s.string().min(3).max(32).require()
.pattern(/^[a-zA-Z0-9_]+$/)
.label('username'),
email: s.email().label('email address').require(),
age: s.number().min(18).max(100).label('age')
});
advantage:
- ✅ Complete builder-method hints
- ✅ IDE automatically prompts all factory and builder methods
- ✅ Best for users who prefer discoverable APIs over compact DSL literals
// ❌ Direct string chains without schema-dsl/string-types
const schema = s({
email: 'email!'.label('email') // There may be no type hint
});
// ❌ Mixing root-entry String side effects with pure-entry examples in the same guide
// Keep one entry style per guide, package, or codebase section.
4. Complete example
import { s, validateAsync, ValidationError } from 'schema-dsl/pure';
//define Schema
const registrationSchema = s({
profile: s({
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('username')
.error({ pattern: 'Can only contain letters, numbers and underscores' }),
email: s('email!')
.label('email address')
.error({ required: 'Email required' }),
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('password')
.error({ pattern: 'Password must be at least 8 characters and must contain letters and numbers' }),
age: s('number:18-100')
.label('age')
}),
settings: s({
emailNotify: s('boolean')
.default(true)
.label('Email Notification'),
language: s('string')
.default('zh-CN')
.label('Language settings')
})
});
// Asynchronous validation (recommended)
async function registerUser(data: any) {
try {
const validData = await validateAsync(registrationSchema, data);
console.log('Registration successful:', validData);
return validData;
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:');
error.errors.forEach(err => {
console.log(` - ${err.path}: ${err.message}`);
});
throw error;
}
throw error;
}
}
// use
registerUser({
profile: {
username: 'testuser',
email: 'test@example.com',
password: 'StrongPass123!',
age: 25
},
settings: {
emailNotify: true,
language: 'en-US'
}
});
4.2 API request validation
import { ValidationError, s, validateAsync } from 'schema-dsl/pure';
import express from 'express';
const app = express();
app.use(express.json());
//Define API Schema
const createUserSchema = s({
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('username'),
email: s('email!').label('mailbox'),
role: s('string')
.default('user')
.label('role')
});
// Use middleware
app.post('/api/users', async (req, res) => {
try {
const validData = await validateAsync(createUserSchema, req.body);
//Create user logic
const user = await createUser(validData);
res.json({ success: true, data: user });
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({
success: false,
errors: error.errors.map(e => ({
field: e.path,
message: e.message
}))
});
} else {
res.status(500).json({ success: false, message: 'Server error' });
}
}
});
import { s } from 'schema-dsl/pure';
//Define common fields
const commonFields = {
email: s('email!')
.label('email address')
.error({ required: 'Email required' }),
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('username')
.error({ pattern: 'Username can only contain letters, numbers and underscores' }),
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('password')
.error({ pattern: 'Password must be at least 8 characters and must contain letters and numbers' })
};
//Registration form
const registrationSchema = s({
...commonFields,
confirmPassword: s('string!')
.label('Confirm password')
});
//Login form
const loginSchema = s({
email: commonFields.email,
password: s('string!').label('password') // No strong password validation is required when logging in
});
// Password reset form
const resetPasswordSchema = s({
email: commonFields.email,
newPassword: commonFields.password,
confirmPassword: s('string!').label('Confirm new password')
});
5. FAQ
5.1 Why are there no type hints for string chain calls in TypeScript?
Cause: TypeScript has restrictions on type inference for the global String.prototype extension.
Solution: Use s('...') to wrap the string:
// ❌ May be silent
'email!'.label('email')
// ✅ Full Tips
s('email!').label('mailbox')
5.2 Do JavaScript users need to change the way they write?
Existing JavaScript users can keep root-entry compatibility code, but new documentation recommends the side-effect-free pure entry:
import { s } from 'schema-dsl/pure';
const schema = s({
email: s('email!').label('mailbox'),
age: 'number:18-100'
});
5.3 How to use it in strict mode?
Enabling strict mode in tsconfig.json is no problem either:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
Just use s('...'):
const schema = s({
email: s('email!').label('mailbox') // ✅ Normal in strict mode
});
5.4 How to obtain the validated data type?
Use generic parameters:
interface User {
username: string;
email: string;
age?: number;
}
// Synchronous validation
const result = validate<User>(userSchema, data);
if (result.valid) {
const user: User = result.data; // ✅ Type safety
}
// Asynchronous validation
const validUser = await validateAsync<User>(userSchema, data);
// ^?User - complete type deduction
5.5 How to handle validation errors for nested objects?
try {
await validateAsync(schema, data);
} catch (error) {
if (error instanceof ValidationError) {
// Method 1: Iterate through all errors
error.errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
// Output: profile.username: username must be at least 3 characters
});
// Method 2: Get specific field errors
const usernameError = error.getFieldError('profile.username');
if (usernameError) {
console.log(usernameError.message);
}
// Method 3: Get all field error mappings
const fieldErrors = error.getFieldErrors();
// { 'profile.username': {...}, 'profile.email': {...} }
}
}
6. Advanced techniques
6.1 Additional business rules
const schema = s({
username: s('string:3-32!').label('username')
});
const result = await validateAsync(schema, data);
if (result.username === 'admin') {
throw new Error('Username already exists');
}
The advantage of this writing method is that schema-dsl is still responsible for structure validation, and rules such as business uniqueness and database duplication checking continue to remain in the TypeScript business layer, avoiding the need to stuff external dependencies into field declarations.
6.2 Condition validation
const schema = s({
userType: s('string!').label('user type'),
// Use s.match() to dynamically verify based on the userType field
companyName: s.match('userType', {
'company': 'string!', // Required for enterprise users
'_default': 'string' // Optional for individual users
})
});
6.3 Schema reuse and extension
import { SchemaUtils, s } from 'schema-dsl/pure';
//Basic user Schema
const baseUserSchema = s({
username: s('string:3-32!').label('username'),
email: s('email!').label('mailbox')
});
//Expand to administrator Schema
const adminSchema = SchemaUtils.extend(baseUserSchema, {
role: s('string!').default('admin').label('role'),
permissions: s('array<string>').label('permission list')
});
// Select only some fields
const publicUserSchema = SchemaUtils.pick(
baseUserSchema,
['username']
);
7.1 Reuse Schema and default cache
const schema = s({
email: s('email!').label('mailbox')
});
// Multiple verifications will reuse the default Validator's compilation cache.
await validateAsync(schema, data1);
await validateAsync(schema, data2);
await validateAsync(schema, data3);
7.2 Cache configuration
import { s } from 'schema-dsl/pure';
//Configure cache size
s.config({
cache: {
maxSize: 5000, //Number of cache entries
ttl: 60000 // Expiration time (milliseconds)
}
});
8. Summary of best practices
- ✅ Choose the authoring entry by goal: pure DSL in
s({}), s('...'), or s.xxx()
- ✅ Use
validateAsync for asynchronous validation
- ✅ Add generic type parameters for validation results
- ✅ Reuse common field definitions
- ✅ Use
ValidationError type guard to handle errors
- ✅ Provide user-friendly error messages
- ✅ Reuse commonly used Schema objects to make the default cache hit
Corresponding sample file
Example entry: typescript-guide.ts
Description: Shows the three recommended TypeScript authoring entries, validate<T>() / validateAsync<T>(), and ValidationError field error reading.