It's not possible to make this work with generics and conditional types in TypeScript because checking a generic value will not change the generic type itself. For that functionality, we would have to wait for something like microsoft/TypeScript#33014 to be implemented.
In cases where you need to check one property to narrow down the type of another property in TypeScript, you can do so when dealing with discriminated unions. However, if your properties are either strings or numbers, they won't serve as discriminants. To resolve this, you may introduce a separate literal type property, such as 'fieldZero', which could help achieve the desired narrowing:
type FooParams =
{ zero: true, one: number, two: Bar[] } |
{ zero: false, one: string, two: Baz[] };
class Foo {
constructor(public params: FooParams) {}
generalFunction() {
console.log(this.params.two.map(x => x.foo).join());
}
specificFunction() {
if (this.params.zero) {
this.processBars(this.params.two);
}
else {
this.processBazs(this.params.two);
}
}
processBars(bars: Bar[]) {
console.log(bars.map(x => x.bar).join());
}
processBazs(bazs: Baz[]) {
console.log(bazs.map(x => x.baz).join());
}
}
This approach makes 'FooParams' a discriminated union and eliminates the need for custom type guard functions.
If you prefer 'Foo' to actually be 'FooParams', not just contain it, things get more complicated. Creating union types from class statements directly in TypeScript is not supported. Despite this limitation, you can achieve a similar behavior by following these steps:
Firstly, rename the 'Foo' class to '_Foo' and define its base supertype:
class _Foo {
constructor(
public fieldZero: boolean,
public fieldOne: number | string,
public fieldTwo: Bar[] | Baz[]
) { }
generalFunction() {
console.log(this.fieldTwo[0].foo);
}
// Other methods
}
Next, declare variants of 'Foo' using interfaces that extend '_Foo':
interface FooTrue extends _Foo {
fieldZero: true,
fieldOne: number,
fieldTwo: Bar[]
}
interface FooFalse extends _Foo {
fieldZero: false,
fieldOne: string,
fieldTwo: Baz[]
}
Now, define 'Foo' as the discriminated union type while maintaining a reference to '_Foo' as the underlying value with appropriate type assertions:
type Foo = FooTrue | FooFalse;
const Foo = _Foo as {
new(fieldZero: true, fieldOne: number, fieldTwo: Bar[]): FooTrue,
new(fieldZero: false, fieldOne: string, fieldTwo: Baz[]): FooFalse
}
With this setup, you can use 'Foo' like a discriminated union within '_Foo'. Ensure to specify the 'this' parameter within internal methods to enforce matching against 'Foo':
specificFunction(this: Foo) {
if (this.fieldZero) {
this.processBars(this.fieldTwo);
}
else {
this.processBazs(this.fieldTwo);
}
}
The above strategy avoids errors but may not be the most optimal solution given TypeScript's design focus on inheritance rather than alternation within a single class. Consider creating subclasses or utilizing union properties if the current implementation feels overly convoluted.
Access Playground link here