Home > Enterprise >  Creating a generic function that receives object with some properties
Creating a generic function that receives object with some properties

Time:09-26

So I want to create an interface which has a property that is a function. This function must receive an object but each function that implements this interface can have different properties on the object that it receives.

This is how I went about doing it:

interface Some {
    fn: (data: Record<string, string>) => string
}

const first: Some = {
    fn: (data: { foo: string }) => `yes ${data.foo}`
}

const second: Some = {
    fn: (data: { bar: string }) => `no ${data.bar}`
}

Though this throws an error:

Type '(data: {    foo: string;}) => string' is not assignable to type '(data: Record<string, string>) => string'.
  Types of parameters 'data' and 'data' are incompatible.
    Property 'foo' is missing in type 'Record<string, string>' but required in type '{ foo: string; }'.

Is there a way to make this work?

CodePudding user response:

You have encountered the issue of function parameter bivariance. You can address it by creating a constrained generic type parameter for your function's data parameter, and requiring it when annotating the Some interface, like so:

TS Playground

interface Some<Data extends Record<string, string>> {
  fn: (data: Data) => string;
}

const first: Some<{ foo: string }> = {
  fn: (data) => `yes ${data.foo}`,
};

const second: Some<{ bar: string }> = {
  fn: (data) => `no ${data.bar}`,
};


In response to your comment:

Is there a way to do it without having a Generic on the entire interface and only on the function instead?

No — it's still an issue of function parameter bivariance.

I think you're probably interested in the satisfies operator (upcoming in TS 4.9, PR at ms/TS#46827). Until then, you could use a constrained identity function with a more relaxed fn type, like in the example below — if the parameter(s) of the fn method aren't relevant to other methods in the object, then you don't need to constrain them in the Some interface and you can just constrain the return type:

TS Playground

interface Some {
  fn: (data: any) => string;
}

function createSome<T extends Some>(some: T): T {
  return some;
}

// OK:
const first = createSome({
    //^? const first: { fn: (data: { foo: string; }) => string; }
  fn: (data: { foo: string }) => `yes ${data.foo}`,
});

// OK:
const second = createSome({
    //^? const second: { fn: (data: { bar: string; }) => string; }
  fn: (data: { bar: string }) => `no ${data.bar}`,
});

// Error:
const third = createSome({
  fn: (data: { baz: string }) => data.baz.length, /*
  ~~
Type '(data: {baz: string;}) => number' is not assignable to type '(data: any) => string'.
  Type 'number' is not assignable to type 'string'.(2322) */
});

CodePudding user response:

Use this:

interface Some<T extends object>{
    fn: (data: T) => string
}

Or this:

interface Some<T extends Record<string, string>> {
    fn: (data: T) => string
}

And then when creating object:

const first: Some<{ foo: string }> = {
    fn: (data) => `yes ${data.foo}`
}
  • Related