2

Is it possible to do something like this in TypeScript?

If not, is there a formal explanation as to why the compiler can't infer nested type parameters?

Here's an example of what I'm trying to achieve:

interface TestFilter {
    name?: string;
}

interface FilterComponent<F> {
    setFilter(filter: F)
}

class TestFilterComponent implements FilterComponent<TestFilter> {
    private filter: TestFilter;

    setFilter(filter: TestFilter) {
        this.filter = filter;
    }
}

// Can I use F without declaring it as another type parameter?
// Something like: class FilterWrapperComponent<FC extends FilterComponent<F>>
abstract class FilterWrapperComponent<F, FC extends FilterComponent<F>> {
    private sidebarFilter: FC;
    private modalFilter: FC;

    public passFilter(filter: F) {
        this.sidebarFilter.setFilter(filter);
        this.modalFilter.setFilter(filter);
    }
}

// I want to just write "extends FilterWrapperComponent<TestFilterComponent>"
// and get TestFilter as F automatically
class TestFilterWrapperComponent extends FilterWrapperComponent<TestFilter, TestFilterComponent> {

}

Also available on playground.

2
  • Please, provide all necessary details in the question itself, this will help the readers to understand the question. Also, no, it's not possible, and your example differs from this one stackoverflow.com/questions/15430701/… , because TestFilterComponent is not FilterComponent<TestFilter>, it just implements it but it's separate class/interface. Commented Mar 23, 2018 at 22:57
  • @estus Updated the question. As for "just implements the interface", I can replace it with abstract class to get "is-a" relationship and but the problem is still there. Commented Mar 24, 2018 at 10:58

3 Answers 3

2

With the infer statement, you can infer types inside conditional types.

That means you can extract types "hidden" in another type. Next, I wrote UnwrapFilter which extracts the Filter generic argument from a FilterComponent type.

type UnwrapFilter<T extends FilterComponent<any>> = T extends FilterComponent<infer TC> ? TC : never;

abstract class FilterWrapperComponent2<FC extends FilterComponent<any>> {
    constructor(
      private sidebarFilter: FC,
      private modalFilter: FC,
    ) {}

    public passFilter(filter: UnwrapFilter<FC>) {
        this.sidebarFilter.setFilter(filter);
        this.modalFilter.setFilter(filter);
    }
}

I'm not sure if a more general version - not relying on the specific FilterComponent type - of UnwrapFilter can be written. That is because the infer statement seems to require T to be a direct instantiation of whatever comes after extends, i.e. in the following example T1 is properly inferred, but T2 isn't.

interface OneParamGeneric<T> {}
type UnwrapOneParam<T extends OneParamGeneric<{}>> = T extends OneParamGeneric<infer TypeArg> ? TypeArg : never;

type T1 = UnwrapOneParam<OneParamGeneric<TestFilter>>;
type T2 = UnwrapOneParam<FilterComponent<TestFilter>>;


In other scenarios a type query - now referred to in the documentation as Indexed Access Types - may be used.

In this example, PublicFilterComponent exposes the activeFilter as a field, thereby allowing access via the square bracket syntax PublicFilterComponent['activeFilter'].

interface PublicFilterComponent<F> {
    public readonly activeFilter: F;
    setFilter(filter: F): void;
}


abstract class FilterWrapperComponent3<FC extends PublicFilterComponent<any>> {
    constructor(
      private sidebarFilter: FC,
      private modalFilter: FC,
    ) {}

    public passFilter(filter: FC['activeFilter']) {
        this.sidebarFilter.setFilter(filter);
        this.modalFilter.setFilter(filter);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

By default, generic parameter value is {}. It applies when a parameter is omitted. It's possible to change default value with with generic parameter defaults.

I want to just write "extends FilterWrapperComponent" and get TestFilter as F automatically

This is possible in the opposite way, to get both FilterComponent<TestFilter> and TestFilter by specifying only TestFilter:

abstract class FilterWrapperComponent<F, FC = FilterComponent<F>> {}

class TestFilterWrapperComponent extends FilterWrapperComponent<TestFilter> {}

4 Comments

Almost what I wanted. The only difference is that I get FilterComponent<TestFilter> so I can't use methods from TestFilterComponent that don't belong to the interface.
In your example, all methods (setFilter) are shared. If TestFilterComponent has methods that FilterComponent doesn't then this is exactly the problem that I mentioned, above, TestFilterComponent is not FilterComponent<TestFilter>, it just implements it but it's separate class/interface. It isn't related to this question in this case, stackoverflow.com/questions/15430701/… .
I.e. there is no direct relationship between TestFilterComponent and TestFilter anymore, because TestFilterComponent isn't generic class. This is exactly what param default serves for. If you need only methods from FilterComponent<TestFilter>, you can omit second param. If you need something special (TestFilterComponent or any other compatible type), second param should be specified.
Right, I didn't add TFC-specific methods to the example. I see, I can either start from filter and be limited by interface for FilterComponent or I can use the implementation of FilterComponent but can't get the filter type automatically.
1

You can in TypeScript 2.8 which is currently in release candidate state and available by installing typescript@rc.

TypeScript 2.8 comes with conditional types and type inference for conditional types. What you actually want is type inference in the generic constraint but that's not directly possible, the workaround is to use a conditional type which resolves to never in the else branch.

interface FilterComponent<F = any> {
    setFilter(filter: F)
}

type ExtractFilter<C extends FilterComponent> = C['setFilter'] extends (filter: infer F) => void ? F : never;

Now you can write your classes like this:

abstract class FilterWrapperComponent<FC extends FilterComponent> {
    private sidebarFilter: FC;
    private modalFilter: FC;

    public passFilter(filter: ExtractFilter<FC>) {
        this.sidebarFilter.setFilter(filter);
        this.modalFilter.setFilter(filter);
    }
}

class TestFilterWrapperComponent extends FilterWrapperComponent<TestFilterComponent> {
}

3 Comments

Looks hacky though. What if I change the setFilter method name? I wonder why didn't they make it less cumbersome (even though it's workaround, it shows that compiler can get the type somehow).
Of course, it'd be much nicer to write class FilterWrapper<FC extends FilterComponent<infer F>>, but according to Anders Hejlsberg "it is not trivial to implement", he's not giving specifics why though. My answer is based on the workaround he proposed.
Thanks for the link, I couldn't find any info in TS docs/developer comments for this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.