I frequently come across this particular pattern while working with the API. It involves having an object that, based on a specified type, will have certain properties that are always required, some that are optional, and some that are only required for a specific type. Typically, I address this issue by using the following approach (which may be clearer with some code examples):
export type FoobarTypes = 'foo' | 'bar';
export interface FooBarBase {
id: string;
type: FoobarTypes;
optional?: any;
}
export interface FooBarFoo extends FooBarBase {
foo: any;
}
export interface FooBarBar extends FooBarBase {
bar: any;
}
export type FooBar = FooBarFoo | FooBarBar;
// differentiating between types:
export const isFooBarFoo = (foobar: FooBar): foobar is FooBarFoo =>
(foobar as FooBarFoo).type === 'foo';
export const FooBarBar = (foobar: FooBar): foobar is FooBarBar =>
(foobar as FooBarBar).type === 'bar';
While this method works well, I can't help but feel that it's somewhat complex and there might be a more efficient way to achieve the same result. Is there a better alternative?
Edit: This is just a refined version of the solution provided by @Fyodor. I'm including it here for easy reference, rather than buried in the comments, in case someone else has a similar question. His answer remains accurate, and I wouldn't have arrived at this version without his input.
export type FoobarTypes = 'foo' | 'bar';
// common properties shared among all types
export interface FooBarBase {
id: string;
optional?: any;
}
// additional type-specific properties based on the type
export type FooBar<T extends FoobarTypes> =
T extends 'foo' ? FooBarBase & {
type: T;
foo: any;
} : T extends 'bar' ? FooBarBase & {
type: T;
bar: any;
} : never;
// example usage...
function FB(fb: FooBar<FoobarTypes>) {
if (fb.type === 'foo') fb.foo = '1';
if (fb.type === 'bar') fb.bar = '2';
}