-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Difficult to implement foo<K extends keyof FooMap>(k: K): FooMap[K] #31904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Ran into a similar issue while upgrading Aurelia's TS version. It actually broke a lot of the project because we're doing various kinds of exotic strongly typed object cloning/overwriting. It seems like the assignment to a It used to be a union type for both cases in Another example to illustrate this problem: interface IThing {
str: string;
num: number;
}
const thing1: IThing = { str: '', num: 0 };
const thing2: IThing = { str: '', num: 0 };
for (const prop in thing2) {
thing1[prop as keyof IThing] = thing2[prop as keyof IThing];
} At some point the types can't be intersected anymore and you get interface IThing {
str: string;
num: number;
bool: boolean;
}
const thing1: IThing = { str: '', num: 0, bool: true };
const thing2: IThing = { str: '', num: 0, bool: true };
for (const prop in thing2) {
thing1[prop as keyof IThing] = thing2[prop as keyof IThing];
} In a nutshell, it's now impossible to dynamically assign to an object using Perhaps https://github.com/aurelia/aurelia would be a good integration test case to add to the test suite. We seem to get breakages every release since 3.3.400 ;) |
It's clear from the error message the compiler is using an intersection type when validating the return expression. It would be great to get some guidance from the TypeScript team on how to implement functions with interfaces like Line 4369 in 81f7153
It seems lib.es6.d.ts contains function interfaces that are impossible to implement without resorting to function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K] {
if (tagName === 'a') {
// Type 'HTMLAnchorElement' is not assignable to type 'HTMLElementTagNameMap[K]'.
// Type 'HTMLAnchorElement' is not assignable to type 'HTMLObjectElement & HTMLFormElement & HTMLAnchorElement & HTMLElement & HTMLAppletElement & HTMLAreaElement & HTMLAudioElement & HTMLBaseElement & HTMLBaseFontElement & ... 62 more ... & HTMLVideoElement'.
// Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLObjectElement': align, archive, border, code, and 21 more.ts(2322)
return new HTMLAnchorElement(); // ❌ ^^^
}
throw new Error('not implemented');
}
const el = createElement('a'); // "el" is inferred to be of type HTMLAnchorElement ✅ |
This was affected by #30769. The issue is that the old behaviour which didn't report an error way clearly wrong: export function getAnimal<T extends keyof AnimalMap>(kind: T): AnimalMap[T] {
switch (kind) {
case 'dog':
return { meow: 'meow' };
case 'cat':
return { bark: 'woof' };
default:
throw new Error('nope');
}
} Ultimately signatures like export function getAnimal<K extends keyof AnimalMap>(kind: K): AnimalMap[K];
export function getAnimal(kind: keyof AnimalMap): AnimalMap[keyof AnimalMap] {
....
} |
@jack-williams That's just solving one use case with a workaround, but ultimately not addressing the underlying (what I believe to be) flaw of the assignment type being turned into an intersection type. Applying that trick, for example, doesn't work in the use case I showed. To continue with dogs & cats: export interface Dog {
bark: string;
}
export interface Cat {
meow: string;
}
export interface AnimalMap {
'dog': Dog;
'cat': Cat;
}
const animalMap1: AnimalMap = { dog: { bark: 'bark1' }, cat: { meow: 'meow1' } } as any;
const animalMap2: AnimalMap = { dog: { bark: 'bark2' }, cat: { meow: 'meow2' } } as any;
for (const kind in animalMap2) {
animalMap1[prop as keyof AnimalMap] = animalMap2[kind as keyof AnimalMap] as AnimalMap[keyof AnimalMap];
} How would you solve this case? And why would the left-hand side and the right-hand side of this expression, which both essentially have the same type, be resolved to different types in the first place? This doesn't seem right no matter how you slice it. |
@ahejlsberg I was reading through #30769 just now and saw the various decisions and justifications. I understand now. Still, currently the only solution seems to be to completely toss out type checking with an When looping over an array of Does that make sense? |
This already works for the same So, your example above works when you write it this way: export interface Dog {
bark: string;
}
export interface Cat {
meow: string;
}
export interface AnimalMap {
'dog': Dog;
'cat': Cat;
}
function assignProps<T extends object>(target: T, source: T) {
for (const p in source) {
target[p] = source[p];
}
}
const animalMap1: AnimalMap = { dog: { bark: 'bark1' }, cat: { meow: 'meow1' } } as any;
const animalMap2: AnimalMap = { dog: { bark: 'bark2' }, cat: { meow: 'meow2' } } as any;
assignProps(animalMap1, animalMap2); See my comments #30769 (comment) and #30769 (comment) for the rationale. |
@ahejlsberg thank you so much for looking at this. I think the spirit of my original question might have been lost in some of the discussion. What I'm looking for is how to implement signatures like Line 4369 in 81f7153
function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K] {
if (tagName === 'a') {
// Type 'HTMLAnchorElement' is not assignable to type 'HTMLElementTagNameMap[K]'.
// Type 'HTMLAnchorElement' is not assignable to type 'HTMLObjectElement & HTMLFormElement & HTMLAnchorElement & HTMLElement & HTMLAppletElement & HTMLAreaElement & HTMLAudioElement & HTMLBaseElement & HTMLBaseFontElement & ... 62 more ... & HTMLVideoElement'.
// Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLObjectElement': align, archive, border, code, and 21 more.ts(2322)
return new HTMLAnchorElement(); // ❌ ^^^
}
throw new Error('not implemented');
}
const el = createElement('a'); // "el" is inferred to be of type HTMLAnchorElement ✅ |
Like @jack-williams implied: function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
function createElement(tagName: keyof HTMLElementTagNameMap, options?: ElementCreationOptions): HTMLElementTagNameMap[keyof HTMLElementTagNameMap] {
if (tagName === 'a') {
return new HTMLAnchorElement();
}
throw new Error('not implemented');
}
const el = createElement('a'); // "el" is inferred to be of type HTMLAnchorElement ✅ |
Sorry for "hijacking" @jdanyow, I thought our issues seemed similar enough in nature that I should post here instead of creating a new issue. I thought your specific use case was addressed in an earlier comment. Anyway, my takeaway from all this is that So in @jdanyow 's case, it's evident that one overload is needed (which still resolves to a single non-overloaded method in the types) to keep the compiler happy for the implementation whilst keeping the same external signature. In my case, an
And I don't believe there is any way to declare a generic type constraint as covariant or contravariant, is there? |
@weswigham the workaround provided by @jack-williams was great because it enabled us to preserve our existing code. I think there's still an opportunity to improve the ergonomics and type safety. Here's an extended example: function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
function createElement(tagName: keyof HTMLElementTagNameMap, options?: ElementCreationOptions): HTMLElementTagNameMap[keyof HTMLElementTagNameMap] {
if (tagName === 'a') {
return new HTMLAnchorElement();
}
if (tagName === 'p') {
return new HTMLCanvasElement(); // 🐛 bug not caught by compiler due to overload signature.
}
throw new Error('not implemented');
}
const el1 = createElement('a'); // "el1" is inferred to be of type HTMLAnchorElement ✅
const el2 = createElement('p'); // 🐛 "el2" is inferred to be of type HTMLParagraphElement but a bug in the method body returned an HTMLCanvasElement |
I'm sorry to necropost here, but this is such a nasty gotcha that I am constantly running into some variant of it and frequently spend hours mucking around with different approaches just to get two objects with identical keys to take an assignment. I understand that allowing this to happen is unsound, but it's such a fundamental operation in javascript and it's so frustrating to not be able to write something as simple as Imagine you are a new front-end developer.
I think this is one of those cases where improving the UX is much simpler and more effective than a technical solution. Perhaps giving this particular issue a unique error that leads from google to a page in the handbook that shows a handful of examples and workarounds, and links to a more in-depth explanation for those that are interested. All I know is that I've been doing this since we were partying like it was 1999, and I still can't quite figure out how to avoid this problem. It's not enough to tell people their code is unsound if we can't tell them how to make it sound. |
A unique error code doesn't sound like a bad idea, but that requires detecting this particular situation in the TS compiler. If I'm not mistaken, that's part of the challenge::
It's currently classified as a design limitation: I don't know if a shortcut might be possible to make it easier. For instance, if an intersection type resolves to |
I just encounter this issue as well. I see it closed but no fix done on it. |
TypeScript Version: 3.5.2, 3.5.1 (didn't test in 3.5.0)
Does not reproduce in 3.4.5
Search Terms:
Code
Expected behavior:
I didn't expect this to be an error when upgrading from 3.4.5 to 3.5.2. If calling
getAnimal
with "dog" infers a return type ofDog
, shouldn't the same inference apply within the getAnimal function body?I learned the pattern for this function signature from looking at the
querySelector
interface in lib.dom.d.tsTypeScript/lib/lib.dom.d.ts
Line 10810 in 81f7153
Actual behavior:
Playground Link note: playground is on older version of typescript, issue doesn't reproduce there
The text was updated successfully, but these errors were encountered: