Reputation: 566
I can use conditional types to conditionally include methods on an interface:
interface Iter<T> {
flatten: T extends Iterable<infer A> ? () => Set<A> : never;
}
Is there a way of doing this with a class as well?
Upvotes: 1
Views: 965
Reputation: 328618
Assuming you're using the --strict
suite of compiler features for a "standard" level of safety, or at least the --strictPropertyInitialization
compiler option, then you need to initialize any class member you have.
But it's essentially impossible for the compiler to verify that a value is assignable to a conditional type that depends on a generic type parameter; it just defers evaluation of the type until such time as the generic type parameter is specified. Something like T extends Iterable<infer A> ? () => Set<A> : never;
is therefore opaque to the compiler, and so there's not really anything you can assign to flatten
without using a type assertion to quell the compiler's concerns about type safety.
So you could do something like this:
class Iter<T> {
constructor(public prop: T) { }
flatten = (function (this: Iter<T>) {
return new Set(this.prop as Iterable<any>)
} as T extends Iterable<infer A> ? () => Set<A> : never;
}
which works:
const a = new Iter(123);
try {
const oops = a.flatten(); // error!
// ----------> ~~~~~~~
// never has no call signatures
} catch (e) {
console.log(e); // this.prop is not iterable
}
const b = new Iter([1, 2, 3]);
const okay = b.flatten();
// const okay: Set<number>
but it's clunky and weird, even aside from the type assertion. You don't really want flatten
to be an instance property, do you? It's more like a method that lives on the class prototype. And you're lying to the compiler because when T
is not iterable, then you claim that flatten
is of type never
which is impossible.
Instead, I'd think what you actually want is for flatten
to be a method that always exists, but is only safe to call when T
is iterable. You can get that effect with a this
parameter:
class Iter<T> {
constructor(public prop: T) { }
flatten<A>(this: Iter<Iterable<A>>) {
return new Set(this.prop)
}
}
That compiles without type assertions or conditional types, and is verified as safe by the compiler. The return type of flatten
is Set<A>
, when called on an Iter<T>
which is assignable to Iter<Iterable<A>>
.
This behaves similarly from the caller's side, and the error is more descriptive of the issue (it isn't that flatten
is never
, but that a
is not an Iter<Iterable<A>>
):
const a = new Iter(123);
try {
const oops = a.flatten(); // error!
// --------> ~
// The 'this' context of type 'Iter<number>' is not assignable
// to method's 'this' of type 'Iter<Iterable<unknown>>'.
} catch (e) {
console.log(e); // this.prop is not iterable
}
const b = new Iter([1, 2, 3]);
const okay = b.flatten();
// const okay: Set<number>
Upvotes: 4