Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Unable to declare an interface that extends Record<string, nonAnyType> with additional support functions. #58256

Closed
ruochenjia opened this issue Apr 19, 2024 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@ruochenjia
Copy link

ruochenjia commented Apr 19, 2024

🔎 Search Terms

interface method index accessor
method index type 'string'

🕗 Version & Regression Information

The bug occurs at the latest stable version published on npm. ts-loader 9.5.1 is also used in the project where this problem occurs.
The earlier versions have not been tested, but they are very likely to have the same behavior.

⏯ Playground Link

No response

💻 Code

export interface RawHeaders extends Record<Exclude <string, "get">, string[]> {
	get(key: string): string[];
}

🙁 Actual behavior

The Record<Exclude<string, "somestring">, string[]> type is being treated as a Record<string, string> type, which causes following error message at the line where I declared the get method:

Property 'get' of type '(key: string) => string[]' is not assignable to 'string' index type 'string[]'.

It also shows the same error message at the same line when using an index accessor without extending Record:

export interface RawHeaders {
	[k: Exclude<string, "get">]: string;
	get(key: string): string[];
}

🙂 Expected behavior

It should not show any errors since the key get is excluded when extending the Record interface.

Additional information about the issue

I have not found any valid solutions that would allow a Record<string, nonAnyType> interface to declare additional methods without making the value of the index accessor any, which I believe most developers would not like since it makes the code unchecked, and so is hard to place documentations. This could also confuse new developers when making a library, since it loses the advantages of using TypeScript over plain JS.

Edit:

The solution of using an intersection type works, but I don't think this design to very logical, as in reality not all keys are allocated by a Record like this. Furthermore built-in Object functions accessible, and can coexist with the interface:

interface K {
	[k: string]: string;
}
let d : K = Object.create(null);
d.toString();

whereas custom method declarations can only be a type, which makes it un-extendable by another interface, and un-implementable by a class.

@MartinJohns
Copy link
Contributor

MartinJohns commented Apr 19, 2024

Exclude<> is used to remove types from a union type. string is not a union type, so you can't remove the literal type "get" from it. So Exclude<string, "get"> is the same as string. See #47178 and many more.

The type you're trying to create can't be represented in TypeScript. This would require negated types: #4196

@ruochenjia
Copy link
Author

If Exclude is not valid, how can I declare methods within an interface without making the value of the index accessor any?

@MartinJohns
Copy link
Contributor

The closest you can get is using an intersection type:
type RawHeaders = { [k: string]: string } & { get(key: string): string[] }

But be aware that this is an unsound type. It's still possible to access the index signature with a key "get" and assign a string to it.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Apr 19, 2024
@ruochenjia
Copy link
Author

This is not a very good solution as the type is not extendable by another interface, and is also not implementable by a class, which is needed in my project.

@ruochenjia ruochenjia changed the title Unable to exclude a specific string from a Record type with key type set to string Unable to declare an interface that extends Record<string, nonAnyType> with additional support functions. Apr 19, 2024
@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2024

I'd consider this a duplicate of #17867. There is no "very good solution" here right now, just various workarounds.

@ruochenjia
Copy link
Author

@jcalz It should not be hard to add a fix, since the methods inherited from the Object interface can already be addressed without any errors:

interface K {
	[k: string]: string;
}
let d : K = {};
d.toString(); // works fine, no type errors

@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2024

Still belongs in #17867

@ruochenjia ruochenjia closed this as not planned Won't fix, can't repro, duplicate, stale Apr 19, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants