-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Index signature is missing in type (only on interfaces, not on type alias) #15300
Comments
Same here for 2.3 |
Seeing this still in 2.4. Here's a playground repro. This appears to be a bug considering implicit index signatures is listed as a feature of v2.0. |
Any update regarding the issue? Still not fixed as of |
Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can't, it's "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we'll consider doing it for interfaces as well if that seems to make sense |
@RyanCavanaugh Thanx for reasoning. Can I say as a rule of thumb to use 'type' for internal interfaces since I don't need augmentation and 'interface' for external to be able to augment ? |
There are some other differences between type aliases and interface that might affect your choice - namely you can |
You can extend types through intersection types. // Interface
interface A { x: number; }
interface B { y: string;}
interface C extends A, B { b: boolean;}
var x: C = { b: true, x: 1, y: "" };
// Type
type TA = { x: number; };
type TB = { y: string; };
type TC = TA & TB & { b: boolean; };
var tx: TC = { b: true, x: 1, y: "" }; |
I have an example where this rule is inconsistent when casting to the expected indexed type: interface MyInterface {
a: number
b: number
}
const doesNotWork: { [key: string]: number } = {} as MyInterface
// Index signature is missing in type 'MyInterface'.
const doesWork: { [key: string]: number } = {} as { a: number, b: number } What is it that is considered different about these that is preventing the first case? Note that this is occurring in 2.7 (see the playground link) |
I was working on a somewhat related example today and I was pretty confused by the results. I tried to create some related situations so I could understand the rules. As I understand:
I tested this in typescript playground: // First case (expected)
type A = {
[x: string]: number
};
type B = {
x: number
};
const b: B = { x: 1 };
const a: A = b; // no error
// Second case (unexpected)
interface C {
[x:string]: number
}
interface D {
x: number
}
const d: D = { x: 1 };
const c: C = d; // error
// Type 'D' is not assignable to type 'C'.
// Index signature is missing in type 'D'.
// Third case (expected)
interface E {
[x: string]: number
}
interface F extends E {
x: number
}
const f: F = { x: 1 };
const e: E = f; // no error
// Fourth case (expected)
interface G {
[x: string]: number
}
type H = {
x: number
};
const h: H = { x: 1 };
const g: G = h; // no error
// Fifth case (maybe expected?)
type I = {
[x: string]: number
}
interface J {
x: number
}
const j: J = { x: 1 };
const i: I = j; // error
// Type 'J' is not assignable to type 'I'.
// Index signature is missing in type 'J'. |
Thanks @patrickneugebauer - I had noticed those rules too, and have resorted to using In my example, changing the interface to a type does indeed get around the error. |
@RafaelSalguero thanks a lot for posting this, at least I found the workaround in this post. @patrickneugebauer thanks for your cases. Indeed weird behavior with interfaces :( |
Ran into this when trying to type a function argument that can have arbitrary keys but a known set of values. Unfortunately, the variable I was trying to pass into the function is generated from graphql-code-generator, so requires a bit more legwork than just changing an interface to a type manually. A quick workaround is to spread the variables before passing it in.
|
This limitation also makes it practically infeasible to use a custom JSON type as a generic constraint for HTTP request libraries. Context: sindresorhus/ky#80 (comment) |
我已收到你的邮件
|
Forever ago, @RyanCavanaugh said,
Could someone walk me through why declaration merging matters again? Imagine a world in which there were no errors in the following code: interface Foo {
a: string;
b: string;
c: string;
}
function yellProps(x: { [k: string]: string }) {
Object.keys(x).forEach(k => console.log(x[k].toUpperCase() + "!!!"));
}
function bar(foo: Foo) {
yellProps(foo); // currently an error, but imagine if it were not
yellProps({ ...foo }); // currently okay
} Now someone comes along and merges something unexpected: interface Foo {
d: number;
} Wouldn't this just produce errors inside function bar(foo: Foo) {
yellProps(foo); // now it should be an error
yellProps({ ...foo }); // currently error
} Isn't that... good? I mean, people who want this feature presumably would be happy with new errors appearing if merges happen. Right now every attempted use of a non-index-signature-having interface in a place that needs an index signature is an error. I'd think the removal of these errors would be worth the potential of some of them coming back due to merging. So I'm obviously not seeing the argument that declaration merging matters... could someone spell it out for me? |
Can we all agree on at least one fundamental concept here, given these types which I have made partial and readonly to eliminate any possible quibbles: type Super = {readonly [_ in string]?: string}
interface Sub {readonly id: string} There is no world in which const sub: Sub = {id: '1234'};
const sup: Super = sub; // TS2322: Type 'Sub' is not assignable to type 'Super' So clearly there is a flaw in the current design. To what extent (removing partial or readonly) is another question. |
Implicit index signatures are useful but unsound. I don't see how partial/readonly affects the issue, and there certainly does seem to be at least one world in which interface SubSub extends Sub {readonly age: number}
const subsub: SubSub = { id: "abc", age: 12 }; // okay
const sub: Sub = subsub; // okay
const sup: Super = sub; // okay?
sup.age?.toUpperCase(); // okay 💥 RUNTIME ERROR This issue is about whether or not the usefulness of allowing implicit index signatures for interfaces outweighs the unsoundness. Personally I think the usefulness does outweigh the unsoundness, just as it apparently does with implicit index signatures for type aliases: type Super = { readonly [_ in string]?: string }
type Sub = { readonly id: string } // type, not interface
interface SubSub extends Sub { readonly age: number }
const subsub: SubSub = { id: "abc", age: 12 }; // okay
const sub: Sub = subsub; // okay
const sup: Super = sub; // okay
sup.age?.toUpperCase(); // okay 💥 RUNTIME ERROR and just as it apparently does for "implicit optional properties" for all object types: interface Foo { a: string }
interface Bar extends Foo { b?: number }
interface Baz extends Foo { b?: string }
const baz: Baz = { a: "", b: "" }; // okay
const foo: Foo = baz; // okay
const bar: Bar = foo; // okay
bar.b?.toFixed() // okay 💥 RUNTIME ERROR And as I said above, I'm not sure I see how declaration merging matters, since merging can already cause existing code to error: interface Here { a: string, b: number }
const here: Here = { a: "", b: 123 } // compiler error now
// ... later
interface Here { c: "HAHA" } which presumably is a good thing, since merging in stuff that invalidates the developer's assumptions should result in errors where those invalidations occur. |
@jcalz good point, didn't think of that example. Agree that the usefulness does outweigh the unsoundness. |
This comment was marked as resolved.
This comment was marked as resolved.
@RyanCavanaugh is there any news on this issue? I would have thought it would have been resolved by now as many more complex things have been built and released already. |
This makes a few small revisions to the new `RequestData` integration: - Switch to using booleans rather than an array of keys when specifying what user data to include. This makes it match all of the other options, and is something I should have just done from the get-go. Given that the integration is new and thus far entirely undocumented, IMHO it feels safe to make what is technically a breaking change here. - Rename the integration's internal `RequestDataOptions` type to `RequestDataIntegrationOptions`, to help distinguish it from the many other flavors of request data functions and types floating around. - Make all properties in `RequestDataIntegrationOptions` optional, rather than using the `Partial` helper type. - Switch the callback which actually does the data adding from being an option to being a protected property, in order to make it less public but still leave open the option of subclassing and setting it to a different value if we ever get around to using this in browser-based SDKs. Because I also made the property's type slightly more generic and used an index signature to do it, I also had to switch `AddRequestDataToEventOptions` from being an interface to being a type. See microsoft/TypeScript#15300. - Rename the helper function which formats the `include` option for use in `addRequestDataToEvent` to more specifically indicate that it's converting from integration-style options to `addRequestDataToEvent`-style options. - Refactor the aforementioned helper function to act upon and return an entire options object rather than just the `include` property, in order to have access to the `transactionNamingScheme` option. - Add missing `transaction` property in helper function's output. - Add tests for the helper function.
This issue is old enough to be in kindergarten by now. Has no consensus been reached or has it just been abandoned? |
我已收到你的邮件
|
I believe that the current behavior of Interfaces is correct. Because I can not assign a type to a much wider type. See "L" letter meaning of SOLID principles. BUT behavior of Types is wrong. Let's say I have an Interface A. interface A {
prop: string;
} I can not assign A to Example: const fn = (record: Record<string, string>) => {
record.value1;
record.value2;
record.value3; // No errors here
} So if I'll pass A to fn it is obvious that an error must occur. Unfortunately at the moment, I can assign Type to Record without error. And I believe this is wrong behavior. type TObj = {
title: string;
}
const obj: TObj = {
title: 'title',
};
const fn = (record: Record<string, string>) => {
record.value1;
record.value2;
record.value3; // No errors here
}
fn(obj); // No error here, but it has to be |
^ Like it or not, implicit index signatures are an intended feature of TypeScript. |
@jcalz Thank you for your comment. Yes, I see that this is intended feature but I bealeve it has wrong implementation. I bealive the root of problem here is how
// example 1
const fn = (record: Record<string, string>) => {
record.value1;
record.value2;
record.value3; // No errors here
} then It means that Therefore, // example 2
interface IObj {
prop: string;
}
const obj: IObj = { prop: 'string' };
const record: Record<string, string> = obj; // We expect this should work fine So if we expect that such a assignment have to be correct then we have to agree that the Record is not "wider" type tyan // example 3
const fn = (record: Record<string, string>) => {
if ('value1' in record) {
record.value1
}
if ('value2' in record) {
record.value2
}
if ('value3' in record) {
record.value3;
}
} It is impossible to have an object with all possible string keys that's why code from To finalize: Abviously that current types logic implementation has a problems and leads people to confusing. So I see two possible ways to fix it.
After long thinking I personally prefer the 2nd option. |
in case it helps anyone, I just found a workaround (essentially converting the declare function foo<T extends Record<string, unknown>>(foo: T): void;
interface SomeInterface {
bar: string;
}
const value: SomeInterface = { bar: 'baz' };
// does not work
foo(value);
// does work and is still typesafe, if a bit cheesy
foo<Pick<SomeInterface, keyof SomeInterface>>(value); and depending on the use case, I think it can be applied to the original issue: interface IndexType {
[key: string]: string;
}
interface doesNotWork {
hola: string;
}
type doWorks = { hola: string };
let y: IndexType;
const correctA = { hola: "hello" };
const correctB: doWorks = { hola: "hello" };
//error should be assignable to y
// const error: doesNotWork = { hola: "hello " }; // this is the original type which fails below
const error: Pick<doesNotWork, keyof doesNotWork> = { hola: "hello " }; // this will work below
y = correctA;
y = correctB;
y = error; // no longer an error
y = {... error}; // original workaround (notes from original workaround: not equivalent since the instance is not the same) |
Nice, @matgr1. I've extracted it into a helper type: const error: IndexOptional<doesNotWork> = { hola: "hello " }; // this will work below
y = error; // no longer an error
y.things = "stuff" // can still accept arbitrary keys |
@matgr1 @lobsterkatie
Those examples work because in Typescript As I explained above this is wrong behavior, because it contradicts to Liskov substitution principle. It is still unsafe (demo): interface IObj {
prop: string;
}
const obj1: IObj = { prop: 'string' };
// TS2322: Type 'IObj' is not assignable to type 'Record<string, string>'. Index signature for type 'string' is missing in type 'IObj'.
const record1: Record<string, string> = obj1;
type IndexOptional<T> = Pick<T, keyof T>
const obj2: IndexOptional<IObj> = { prop: 'string' };
// No error because now obj2 has "Type" typing, not an "Interface"
const record2: Record<string, string> = obj2;
// It is still unsafe
const fn = (arg: Record<string, string>) => {
arg.someUnexistedProp;
arg.anotherUnexistedProp;
arg.anyOtherPropertyWhatEverYouWant;
arg.stillNoErrorButWhy;
}
// You have obj2 with only one property? Yes!
// Can you use it in function with a lot of random properties? Yes.
// Is it safe? No!
fn(obj2); |
@Webbrother Yes, TypeScript is intentionally unsound in places; implicit index signatures and unchecked indexed accesses are two of these places, and not really the subject of this GitHub issue. This issue is just about whether or not interfaces should be granted implicit index signatures, with all the accompanying baggage, for better or worse. |
@Webbrother - yes, I understand it's a workaround (and what it's doing). It's just now a slightly nicer-looking workaround. 🙂 And yes, exactly what @jcalz said (though he said it better and more concisely than I could have, so thanks for that, @jcalz!). |
As I said above, before my preschooler was born 😅:
This has not changed. Changing the behavior at this point would honestly be an incredibly disruptive breaking change without much concrete upside. For clarity, I'm going to close and lock this - interfaces should declare an explicit index signature if they intend to be indexed. |
TypeScript Version: 2.2.2
Code
Expected behavior:
The code should not result on a compiler error since the interface
doesNotWork
is equivalent to the type{ hola: string }
Actual behavior:
Variable
error
of typedoesNotWork
can't be assigned to yThe text was updated successfully, but these errors were encountered: