My approach is to focus on creating a framework-agnostic solution. The goal is to address how TypeScript can restrict passing values of types with additional fields, emphasizing the need for exclusive behavior within the type itself.
It's important to note that enforcing such behavior solely at the type level may not always be practical. While compile-time checks can ensure type consistency, runtime validation becomes necessary when dealing with data from APIs.
To implement exclusive type behavior at the type level, consider the following:
type IUser = {
name: string;
lastname: string;
}
type OnlyExistingFields<Base, Extender> = {
[K in keyof Extender]: K extends keyof Base ? Extender[K] : never
}
type IMoreUser = IUser & {
age: number;
}
const user1: IUser = {
name: 'John',
lastname: 'Doe'
}
const user2: IMoreUser = {
name: 'Jane',
lastname: 'Doe',
age: 30
}
declare function insertUser<U extends OnlyExistingFields<IUser, U>>(user: U): void
insertUser(user1) // 👍 correct type
insertUser(user2) // 🔴 error due to extra field
The above implementation ensures that only arguments of a specific type (or its exact subset) are accepted, maintaining type exclusivity.
While the type-level check is effective, it's crucial to acknowledge that runtime enforcement presents challenges. A runtime converter can help eliminate unwanted fields:
// Converter function to extract only IUser fields from an object
function toIUser<T extends IUser>(input: T): IUser {
return {
name: input.name,
lastname: input.lastname
}
}
const convertedUser = toIUser(user2); // convertedUser now has only IUser fields