Home > Software engineering >  TypeScript Generics with interface parameters
TypeScript Generics with interface parameters

Time:12-07

I'm playing around with TypeScript generics and am a bit confused.

I'm basically trying to create an interface that has a method that can receive an arbitrary options parameter. This parameter can be any object. What the exact object looks like is determined by the implementing class.

interface MyOptions {
    foo: string
}

interface TestInterface {
    doSome<T extends Record<string, unknown>>(value: T): void
}

class TestClass implements TestInterface {
    doSome<T = MyOptions>(value: T): void {
        value.foo // complains that foo doesn't exist
    }
}

Everything looks fine, but when I try to access value.foo, it looks like value isn't typed.

Am I doing something wrong?

UPDATE

I found some useful stuff regarding interfaces not extending Record<string, unknown>, saying to use a type instead (see interface object couldn't extends Record<string, unknown>).

However, after updating the snippet above as shown below, the issue remains.

type MyOptions = {
    foo: string
}

interface TestInterface {
    doSome<T extends Record<string, unknown>>(value: T): void
}

class TestClass implements TestInterface {
    doSome<T = MyOptions>(value: T): void {
        value.foo // complains that foo doesn't exist
    }
}

CodePudding user response:

You cannot overload the function in that way. If you are trying to create a union for this, then you can use the following code example:

type MyOptions = Record<string, unknown> & { foo: string };

interface TestInterface {
  doSome<T extends MyOptions>(value: T): void;
}

class TestClass implements TestInterface {
  doSome<T extends MyOptions>(value: T): void {
    console.log(value.foo);
  }
}

const test = new TestClass();

test.doSome({ foo: "bar", baz: "qux" });

/*
*  The combined type of the two objects is:
* TestClass.doSome<{
    foo: string;
    baz: string;
}>(     value: {foo: string, baz: string}): void
* 
* */

Useful link for Dos & Don'ts for overloading link

UPDATE

I believe I understand what you are looking for; the issue you are running into is that Typescript does not know what properties of T are within your function or class. The compiler could not prove that the foo property is on every type. You have two options, either add a primitive type to the argument or apply a generic constraint through the extends keyword:

type MyOptions = {
  foo: string;
};

type MyOtherOptions = {
  bar: string;
};

interface TestInterface {
  doSome(value: unknown): void;
}

class TestClass<T extends Record<string, unknown>> implements TestInterface {
  doSome = (value: T): void => {
    console.log(value.foo);
  };
}

const test = new TestClass<MyOtherOptions>();
test.doSome({ bar: "baz" });

const test2 = new TestClass<MyOptions>();
test2.doSome({ foo: "bar" });

CodePudding user response:

There is an important difference between generic call signatures and generic types. A generic call signature has the generic type parameters on the call signature itself, like this:

interface TestInterfaceOrig {
    doSome<T extends object>(value: T): void
}

When you have a generic call signature, the caller gets to specify the type argument:

declare const testInterfaceOrig: TestInterfaceOrig;
testInterfaceOrig.doSome<{ a: string, b: number }>({ a: "", b: 0 });
testInterfaceOrig.doSome({ a: "", b: 0 });

In the above, the caller is able to choose that T is {a: string, b: number} in both calls. In the second call, the compiler infers that type from the value argument, but it's the same... the caller is in charge. The implementer, on the other hand, has to write doSome() so that it works for any T the caller chooses:

const testInterfaceOrig: TestInterfaceOrig = {
    doSome<T extends object>(value: T) {
        console.log(value); //            
  • Related