简体   繁体   中英

Multiple generic constraints with defaults relying on each other

This is my Typescript interface/class structure that I have:

interface IBaseOptions {
    homeUrl: string;
}

abstract class BaseApp<TOptions extends IBaseOptions = IBaseOptions> {
    constructor(
        private options: TOptions
    ) {
        // does nothing
    }
}

// -----------------------------

interface ICalls {
    getValue: () => number;
}

interface IOptions<TCalls extends ICalls = ICalls> extends IBaseOptions {
    calls: TCalls;
}

class App<TOptions extends IOptions<TCalls> = IOptions<TCalls>, TCalls extends ICalls = ICalls> extends BaseApp<TOptions> {
                                   -------- (ts2744 error here)
    constructor(options: TOptions) {
        super(options);
    }
}

class SubApp extends App {
    // whatever implementation
}

I would like to provide the defaults so that I don't have provide concrete types for my options and calls. The way I defined my types results in a compile error (ts2744 error).

I would also like to avoid swapping my generic types (with constraints and defaults) so that I would keep the first generic type to be options and calls second.

Is there any way to first define generic types with contraints and afterwards set their defaults?

You can check this Playground Link

The obvious fix, swap the order of the type parameters, doesn't work for you. So we have to resort to less obvious fixes. The general idea here is: if you can't set the default value to what you want, set it to a dummy value, and then later when using the type, check for the dummy value and use your originally desired default instead. So Foo<T=Default<U>, U=X> ... T becomes something like Foo<V=DefaultSigil, U=X> ... V extends DefaultSigil ? Default<U> : V Foo<V=DefaultSigil, U=X> ... V extends DefaultSigil ? Default<U> : V .

Here's one way to do it:

type OrDefault<T> = [T] extends [never] ? IOptions<ICalls> : T;

class App<O extends IOptions<C> = never, C extends ICalls = ICalls>
    extends BaseApp<OrDefault<O>> {
    constructor(options: OrDefault<O>) {
        super(options);
    }
}

In this case we're using never as the dummy default; unless someone manually specifies never for O , it's a safe value to use as a dummy. Then OrDefault checks to see if its parameter is never or not, and returns IOptions<ICalls> if so.

Yes, you've noticed that OrDefault 's check is [T] extends [never] ? ... : ... [T] extends [never] ? ... : ... instead of T extends never ? ... : ... T extends never ? ... : ... . The reason I did that is to avoid distributing the conditional type across T . Since T is a "naked type parameter", when you check it like T extends never ? ... : ... T extends never ? ... : ... the compiler will try to interpret T as a union, split the union into members, perform the conditional, and then unite the results back into a union. If you pass in never for T , this will be seen as an "empty union" and the result will always be never . We don't want OrDefault<never> to be never , so we don't want distributive conditional types. The easiest workaround is to prevent the check from being a "naked" type parameter by, uh, "clothing" it in a one-tuple. [T] is not broken into union constituents, and therefore OrDefault<never> will be IOptions<ICalls> as desired.

And instead of using O later, we use OrDefault<O> . You should be able to verify that this works the way you want. It's clunky and convoluted, but it works.


Okay, hope that helps; good luck!

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM