Skip to content
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

Some information is lost when working with keyof against type arguments #48989

Closed
anton-johansson opened this issue May 6, 2022 · 5 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@anton-johansson
Copy link

Bug Report

🔎 Search Terms

Hard topic to search for IMO, but I tried phrases like typescript generic interface lose type information and I only found similar issues but not related.

🕗 Version & Regression Information

I was having trouble using version 4.5.5 (that we're currently using), but I could also reproduce it in the latest version available in the playground (4.6.2). I checked versions back to 3.3.3 in the playground and they all reported the same issue.

⏯ Playground Link

Playground link with relevant code

💻 Code

// BEGIN FRAMEWORK STUFF

type ObjectKey<O, T> = {[K in keyof O]: O[K] extends T ? K : never}[keyof O & string];

interface MyInterface {
    myStringValue: string;
    myIntegerValue: number;
}

abstract class AbstractBase<O> {
    constructor(private readonly object: O) {}

    public getValue<T>(fieldName: ObjectKey<O, T>) {
        return this.object[fieldName];
    }
}

// END FRAMEWORK STUFF

// BEGIN IMPLEMENTATION STUFF

class MyType implements MyInterface {
    myStringValue = "";
    myIntegerValue = -1;
}

class MyInterfaceHolder1<I extends MyInterface> extends AbstractBase<MyInterface> {
    constructor(object: I) {
        super(object);
        this.getValue<number>("myIntegerValue");
    }
}
class MyInterfaceHolder2<I extends MyInterface> extends AbstractBase<I> {
    constructor(object: I) {
        super(object);
        this.getValue<number>("myIntegerValue"); // Argument of type 'string' is not assignable to parameter of type 'ObjectKey<I, number>'.(2345)
    }
}

// END IMPLEMENTATION STUFF

const holder1 = new MyInterfaceHolder1<MyType>(new MyType());
const value1 = holder1.getValue<number>("myIntegerValue");

const holder2 = new MyInterfaceHolder1<MyType>(new MyType());
const value2 = holder2.getValue<number>("myIntegerValue");

🙁 Actual behavior

I lose some type information within the super-class when working with a generic argument.

🙂 Expected behavior

I feel like it should be able to maintain type information here.

@MartinJohns
Copy link
Contributor

Resolving of conditional types is deferred when generic type arguments are involved. It is not using the type that your argument extends (MyInterface in your example). So what type does ObjectKeys<I, number> resolve to? The compiler doesn't know, because it doesn't know what type I is.

@anton-johansson
Copy link
Author

Resolving of conditional types is deferred when generic type arguments are involved. It is not using the type that your argument extends (MyInterface in your example). So what type does ObjectKeys<I, number> resolve to? The compiler doesn't know, because it doesn't know what type I is.

@MartinJohns, thanks for the quick reply! As you can see further down the code, it does work when used outside of the class, but I guess conditional types have been resolved by then.

I understand that there's a lot of underlying complexity and reasoning for all this, but is there a way around this? Is this something that might be "improved" in a future version? Or is it just not possible to achieve?

I am not originally the author of the ObjectKey type, and probably need to learn how everything works there in order to fully understand this.

@MartinJohns
Copy link
Contributor

As you can see further down the code, it does work when used outside of the class, but I guess conditional types have been resolved by then.

Yes, in that case you're not dealing with an unbound generic type anymore, but with a concrete one. The compiler now knows what type O (from the base class) is and can resolve the conditional type.

I understand that there's a lot of underlying complexity and reasoning for all this, but is there a way around this? Is this something that might be "improved" in a future version? Or is it just not possible to achieve?

Who knows when Anders will cast his magic and improve the language in unpredictable ways. But what you're attempting is unsound behaviour. You're assuming That the key "myIntegerValue" refers to the type number, but it doesn't have to be. The type { myStringValue: 'abc'; myIntegerValue: 123 } fulfils the generic constraint extends MyInterface, but the type of myIntegerValue is 123 and not number.

And from this brief example I can't see why you'd need a generic type anyway, when you can just use MyInterface.

@RyanCavanaugh
Copy link
Member

There's no way for TS to establish the higher-order relationship between what goes into type ObjectKey<O, T> = {[K in keyof O]: O[K] extends T ? K : never}[keyof O & string]; and how that relates to the provided keys. We'd need some built-in operator to be able to deduce that this is safe; being "safe by construction" is not something the checker can recognize.

@RyanCavanaugh
Copy link
Member

Got around to logging #48992

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label May 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants