Ginden
Ginden

Reputation: 5316

Partial<T>, but extended with methods

I want to create generic type (with parameter T), that allows following behavior:

I've tried this code:

type PartialOrFunction<T> = {
  [K in keyof T]?: T[K] | ((params?: Partial<T>) => string)
} & {
  [K: string]: ((params?: Partial<T>) => string);
};

But it rejects Partial<T> (TS2345).

Then I've tried this code:

type PartialOrFunction<T> = {
  [K in keyof T]?: T[K] | ((params?: Partial<T>) => string)
} & {
  [K: string]: ((params?: Partial<T>) => string) | any;
};

But it doesn't perform type checking on extraneous properties (because it allows any).

Upvotes: 0

Views: 195

Answers (1)

jcalz
jcalz

Reputation: 328362

In case you want the generic-constraint version of PartialOrFunction I mentioned in the comments, here it is.

Your PartialOrFunction<T> is a specific case of something I'd like to call DefaultProperty<T, D> which takes an object type T with no index signature and adds a "default" property of type D. This is different from a string index signature, which would require that all properties in T be assignable to D. But as you've seen, TypeScript does not support that.

In order to do that you'd need to be able to negate types to exclude keyof T from string, and you'd need to be able to use such negated types as the key type of an index signature. Right now you can't do either. So for DefaultProperty<T, D> you want to say something like:

// this is not valid TypeScript (yet?)
type DefaultProperty<T, D> = T & { [k: string & not keyof T]: D }

but you can't. The closest you could get would be the

type MakeshiftDefaultProperty<T, D> = T & {[k: string]: D | T[keyof T]}

but this is too wide, as it allows unwanted things in your extra properties. If you can handle that, then great. Otherwise, read on:

So what you can do, is to make a type VerifyDefaultProperty<T, D, C> which takes a candidate type C and return a new type which would be like DefaultProperty<T, D>. If C is assignable to VerifyDefaultProperty<T, D, C>, then C is a valid DefaultProperty<T, D>. If C is not assignable to VerifyDefaultProperty<T, D, C>, then the places where they differ are the places in C that are a problem.

// caveat: T should probably not already have an index signature
type VerifyDefaultProperty<T extends object, D, C> =
    { [K in keyof C]: K extends keyof T ? T[K] : D }

For example:

interface T {x: number, y: string};
type D = boolean;
interface CGood {x: 0, y: "y", z: true};
type VGood = VerifyDefaultProperty<T, D, CGood>;
// type VGood = {x: number, y: string, z: boolean}
// CGood is assignable to VGood: yay!
interface CBad {x: 0, y: "y", z: true, oops: 123};
type VBad = VerifyDefaultProperty<T, D, CBad>;
// type VBad = {x: number, y: string, z: boolean, oops: boolean}
// CBad is *not* assignable to VBad: boo!
// specifically, CBad['oops'] is not assignable to boolean

Now, since TypeScript doesn't support DefaultProperty<T, D>, it also doesn't support PartialOrFunction. But we can take VerifyDefaultProperty<T, D, C> and make a VerifyPartialOrFunction<T, C> from it, which acts the same way. That is, C is assignable to VerifyPartialOrFunction<T, C> if and only if C would be a valid PartialOrFunction, and any deviation can be used in an error message:

type VerifyPartialOrFunction<T, C> = VerifyDefaultProperty<
    { [K in keyof T]?: T[K] | ((params?: Partial<T>) => string) },
    ((params?: Partial<T>) => string),
    C
>;

Finally we introduce a helper function that takes a parameter of type C and returns it, but throws a compile-time error if C is not a valid VerifyPartialOrFunction<T, D, C>. Note that this function must be curried, because you want to pass in the type T manually and have the type C inferred automatically, but TypeScript doesn't support partial type parameter inference as of TS3.5. So we need to break it into two functions:

const verifyPartialOrFunction = <T>() =>
    <C>(x: C & VerifyPartialOrFunction<T, C>): C =>
        x;

And now, let's test it:

// Tests
interface Foo {
    a: string,
    b: number,
    c: boolean
}

const goodPartialOrFunctionFoo = verifyPartialOrFunction<Foo>()({
    a: (x?: Partial<Foo>) => "a",
    b: 1,
    w: () => "w"
}); // okay

const badPartialOrFunctionFoo = verifyPartialOrFunction<Foo>()({
    a: 1, // error!
    // Type 'number' is not assignable to type 
    // 'number & ((params?: Partial<Foo> | undefined) => string)'.
    b: (x: string) => "oops", // error!
    // Type '(x: string) => string' is not assignable to type 
    // '((x: string) => string) & 
    //  (number) | ((params?: Partial<Foo> | undefined) => string))'.
    w: "w"; // error!
    //  Type 'string' is not assignable to type 
    // 'string & (params?: Partial<Foo> | undefined) => string'.
})

That looks like the behavior you want.


So that's good as far as it goes. As you can see, it drags a lot of type system fudging around with it, and every value/function/type that depends on PartialOrFunction is now going to have to deal with an extra generic type parameter. This can be tedious, so I generally only recommend this kind of work on exposed APIs that users will call to use your library. Inside your library you should just carefully use the widened types like

type MakeshiftPartialOrFunction<T> = Partial<T> & { [k: string]: 
  T[keyof T] | undefined | ((params?: Partial<T>) => string)
}

and employ type assertions when you know something is safe but the compiler doesn't.


Link to code

Hope that helps; good luck!

Upvotes: 1

Related Questions