Implementing a Flexible Contract Type System in TypeScript
Imagine building a system where different types of contracts (e.g., employment, service, lease) need to be managed, each with its own unique set of properties and validation rules. This challenge focuses on creating a robust and type-safe system in TypeScript to represent and manage these diverse contract types efficiently.
Problem Description
You are tasked with designing and implementing a TypeScript system to represent various contract types. This system should allow for:
- Defining Base Contract Properties: A common set of properties that all contracts share (e.g.,
id,startDate,endDate,status). - Defining Specific Contract Types: Creating distinct "types" of contracts, each with its own set of additional, specific properties. For example, an
EmploymentContractmight haveemployeeNameandposition, while aLeaseContractmight havepropertyNameandrentAmount. - Type Safety and Validation: Ensuring that when you work with a specific contract type, you can only access its defined properties and that basic validation can be performed on these properties.
- Extensibility: The system should be designed to easily add new contract types in the future without significant refactoring.
Key Requirements:
- Define a
BaseContractinterface or type that includes common fields. - Define at least two specific contract types (e.g.,
EmploymentContract,LeaseContract) that extend or compose theBaseContract. - Implement a mechanism to represent the "type" of a contract, allowing you to differentiate between them.
- Provide a function or method that can take a contract object and, based on its type, access its specific properties in a type-safe manner.
- Implement a simple validation function for at least one of the specific contract types.
Expected Behavior:
- When creating an
EmploymentContract, it should have allBaseContractproperties plus its own specific properties. - When trying to access a property that doesn't exist on a specific contract type (e.g., trying to access
propertyNameon anEmploymentContractobject directly), TypeScript should raise a compile-time error. - A function designed to process contracts should be able to correctly infer and use the specific properties of a contract based on its type.
- The validation function should return
truefor valid contracts andfalsefor invalid ones, flagging specific issues.
Edge Cases:
- Handling contracts that might be "null" or "undefined" before they are fully constructed or retrieved.
- Consider how to handle contracts where the type information might be missing or ambiguous. (For this challenge, assume the type is present and correct).
Examples
Example 1: Defining Contract Types
// Input (Conceptual - this is how you'd define the types)
interface BaseContract {
id: string;
startDate: Date;
endDate: Date;
status: 'active' | 'expired' | 'cancelled';
}
enum ContractType {
Employment = 'employment',
Lease = 'lease',
}
interface EmploymentContract extends BaseContract {
type: ContractType.Employment;
employeeName: string;
position: string;
salary: number;
}
interface LeaseContract extends BaseContract {
type: ContractType.Lease;
propertyName: string;
tenantName: string;
rentAmount: number;
leaseTermMonths: number;
}
// A union type to represent any contract
type Contract = EmploymentContract | LeaseContract;
// --- Implementation Goal ---
// Create functions that can work with these types.
Example 2: Working with Contracts and Type Guards
// Input (A concrete contract object)
const myEmploymentContract: EmploymentContract = {
id: 'emp-123',
startDate: new Date('2023-01-01'),
endDate: new Date('2024-12-31'),
status: 'active',
type: ContractType.Employment,
employeeName: 'Alice Smith',
position: 'Software Engineer',
salary: 80000,
};
// Input (Another concrete contract object)
const myLeaseContract: LeaseContract = {
id: 'lease-456',
startDate: new Date('2023-05-01'),
endDate: new Date('2024-04-30'),
status: 'active',
type: ContractType.Lease,
propertyName: '123 Main St',
tenantName: 'Bob Johnson',
rentAmount: 1500,
leaseTermMonths: 12,
};
// --- Implementation Goal ---
// A function that processes contracts and uses type guards.
function getContractDetails(contract: Contract): string {
switch (contract.type) {
case ContractType.Employment:
// TypeScript knows this is an EmploymentContract here
return `${contract.employeeName} is a ${contract.position} earning $${contract.salary}.`;
case ContractType.Lease:
// TypeScript knows this is a LeaseContract here
return `Property at ${contract.propertyName} is leased by ${contract.tenantName} for $${contract.rentAmount}/month.`;
default:
// Handle unexpected contract types (though our union should prevent this)
return `Unknown contract type: ${contract.type}`;
}
}
// Expected Output for getContractDetails(myEmploymentContract):
// "Alice Smith is a Software Engineer earning $80000."
// Expected Output for getContractDetails(myLeaseContract):
// "Property at 123 Main St is leased by Bob Johnson for $1500/month."
Example 3: Validation
// Input (A valid Lease Contract)
const validLease: LeaseContract = {
id: 'lease-789',
startDate: new Date('2024-01-01'),
endDate: new Date('2025-01-01'),
status: 'active',
type: ContractType.Lease,
propertyName: '456 Oak Ave',
tenantName: 'Charlie Brown',
rentAmount: 2000,
leaseTermMonths: 12,
};
// Input (An invalid Lease Contract - missing tenant name)
const invalidLease: Partial<LeaseContract> = { // Using Partial for illustration of missing fields
id: 'lease-999',
startDate: new Date('2024-01-01'),
endDate: new Date('2025-01-01'),
status: 'active',
type: ContractType.Lease,
propertyName: '789 Pine Ln',
rentAmount: 1800,
leaseTermMonths: 6,
};
// --- Implementation Goal ---
// A validation function for LeaseContracts.
function isValidLeaseContract(contract: LeaseContract): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!contract.tenantName || contract.tenantName.trim() === '') {
errors.push('Tenant name is required.');
}
if (contract.rentAmount <= 0) {
errors.push('Rent amount must be positive.');
}
if (contract.leaseTermMonths <= 0) {
errors.push('Lease term must be positive.');
}
// Add more checks as needed...
return {
isValid: errors.length === 0,
errors: errors,
};
}
// Expected Output for isValidLeaseContract(validLease):
// { isValid: true, errors: [] }
// Expected Output for isValidLeaseContract(invalidLease as LeaseContract): // Type assertion for demonstration
// { isValid: false, errors: ["Tenant name is required."] }
Constraints
- Your solution must be written entirely in TypeScript.
- You must define at least two distinct contract types beyond the base contract.
- You must use TypeScript's type system effectively for compile-time safety.
- The solution should include at least one type guard function or a similar mechanism to differentiate contract types at runtime.
- The solution should include at least one validation function for one of the specific contract types.
- Code readability and maintainability are important.
Notes
- Consider using discriminated unions (also known as tagged unions) in TypeScript, as they are a powerful pattern for this type of problem.
- Think about how you might represent optional fields or different validation rules for different statuses of a contract.
- The
Partialutility type in TypeScript can be helpful for demonstrating cases where fields might be missing during development or for error conditions, but your final functions should expect valid types. - Focus on demonstrating the core concepts of defining and differentiating contract types with strong typing.