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

Wrong keyof behaviour for generic with extends types in 2.9 #24560

Closed
mctep opened this issue Jun 1, 2018 · 10 comments
Closed

Wrong keyof behaviour for generic with extends types in 2.9 #24560

mctep opened this issue Jun 1, 2018 · 10 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@mctep
Copy link

mctep commented Jun 1, 2018

TypeScript Version: 2.9.1, 3.0.0-dev.20180601

Search Terms: keyof 2.9 generics extract extends string number symbol

Code

type StringKeyof<T> = Extract<keyof T, string>;

type Omit<T, K extends StringKeyof<T>> = any;

type WithoutFoo = Omit<{ foo: string }, "foo">; // ok

type WithoutFooGeneric<P extends { foo: string }> = Omit<P, "foo">; // Error: Type '"foo"' does not satisfy the constraint 'Extract<keyof P, string>'.

Expected behavior:

Omit should pass "foo" as valid type for keyof because generic type extends a type that has this string key.

Actual behavior:

Error: Type '"foo"' does not satisfy the constraint 'Extract<keyof P, string>'.

Playground Link: Link

@7sempra
Copy link

7sempra commented Oct 28, 2018

This is also true for function-bound generics:

interface Foo {
    foo: number,
}

function run<T extends Foo>() {
    // ERROR: `Type '"foo"' is not assignable to type Extract<keyof T, string>`
    let key: Extract<keyof T, string> = 'foo';

    // Okay
    let key2: Extract<keyof Foo, string> = 'foo';
}

Playground

@weswigham
Copy link
Member

let key: Extract<keyof T, string> = 'foo'; shouldn't work. Substitute never for T (which is allowable), and you'd be attempting to assign 'foo' to never, which shouldn't work.

@7sempra
Copy link

7sempra commented Oct 28, 2018

Huh, interesting, how is that different from using a bare keyof?

function run<T extends Foo>() {
    // Okay
    let key: keyof T = 'foo';
}

If T is never in this case, keyof T is also never, so we're still assigning 'foo' to never.

@hpohlmeyer
Copy link

hpohlmeyer commented May 10, 2019

Is this the same issue?

type FormFields = { someField: string };

// Typescript should allow only keys of "FormFields" to be
// used as fieldName 
class GenericForm<T extends FormFields> {
    public fieldA = new FormField<T>({ fieldNameA: "someField" }); // works as expected
    public fieldB =  new FormField<T>({ fieldNameB: "someField" }); // throws an error
}

interface FormFieldOptions<T extends FormFields> {
    fieldNameA?: keyof T;
    fieldNameB?: Extract<keyof T, string>;
}
class FormField<T extends FormFields> {
    public fieldNameA: string;
    public fieldNameB: string;

    constructor(options: FormFieldOptions<T>) {
        // Does not work because fieldNameA is of type "symbol | number | string" (expected)
        this.fieldNameA = options.fieldNameA;

        // Does work because fieldNameB is ensured to be a string (expected)
        this.fieldNameB = options.fieldNameB;
    }
}

Playground Link

What I would have expected:

  • keyof T
    • possible values: "someField"
    • possible type: symbol | number | string
  • Extract<keyof T, string>
    • possible values: "someField"
    • possible type: string

@weswigham
Copy link
Member

Please note: my older comment on this issue is incorrect - keyof never is now string | number | symbol, so Extracting anything from unknown is just the thing extracted.

@amoscatelli
Copy link

amoscatelli commented Aug 20, 2019

Is this the same issue ?

function run<T extends { foo: string }>() {
  let key: {
    [K in keyof { foo: string }]:
    { foo: string }[K] extends String ? K : never
  }[keyof { foo: string }];
  key = 'foo'; // OK

  let key2: {
    [K in keyof T]:
    T[K] extends String ? K : never
  }[keyof T];
  key2 = 'foo'; // NOT OK
}

@jack-williams
Copy link
Collaborator

@amoscatelli No, the issue there is that conditional types ignore the constraints on type parameters when checking assignability, so T[K] extends string ? K : never does not get simplified to K.

@princefishthrower
Copy link

princefishthrower commented Feb 3, 2022

Knock knock three years later 😄

What is the update, if any, on this? Extract with keyof Something works as expected for any concrete Something, but as soon as you throw generics into the mix Extract doesn't enforce anything at all... it still is effectively keyof T. I feel like IntelliSense should be able to figure this out, i.e. looking at a simple generic isAGreaterThanB function:

// only extract those types from keyof T which make sense with the '>' operator
function isAGreaterThanB <T>(a: T, b: T, compareOnProperty: Extract<keyof T, string | Date | number>) {
  return a[compareOnProperty] > b[compareOnProperty];
}

(Obviously I understand IntelliSense can't "figure it out" with just this function definition only, but as soon as it is provided with variables that carry a concrete type, it seems obvious, see my Full Playground example.)

@carlvincentld
Copy link

carlvincentld commented Mar 14, 2022

@princefishthrower Your issue is not related to the original issue. I think you might have misunderstood the usage of the keyword Extract or keyof T.

Taking back the interface ISomething with slight alteration to the names:

interface ISomething {
    foo: string;
    bar: Date;
    baz: number;
    qux: () => void;
    quux: { foo: string; };
    corge: symbol;
}

type Keys = keyof ISomething; // 'foo' | 'bar' | 'baz' | 'qux' | 'quux' | 'corge';
type ExtractOfKeys = Extract<Keys, string | Date | number>; // 'foo' | 'bar' | 'baz' | 'qux' | 'quux' | 'corge';

Since the keys of ISomething are all strings, Extract returns the full list of keys.

My guess is you want only the keys foo, bar or baz, since they map to are of ordered types. Instead of Extract, you might be looking to use the following "FieldOfType" type:

type FieldOfType<Type, ValueType> = { [K in keyof Type]: Type[K] extends ValueType ? K : never }[keyof Type];
function isAGgreaterThanB<T>(a: T, b: T, compareOnProperty: FieldOfType<T, string | Date | number>) {
    return a[compareOnProperty] > b[compareOnProperty];
}

type FieldsOfType = FieldOfType<ISomething, string | Date | number>; // 'foo' | 'bar' | 'baz'

Here's a Playground Link if you want to play with it.

EDIT: The keys map to ordered types instead of are.

@RyanCavanaugh
Copy link
Member

We're very unequipped to deal with this sort of code.

The core question being posed this code is, in essence, does any possible instantiation of WithoutFooGeneric produce a type such that Extract<keyof P, string> produces a supertype of "foo" ?

That's sort of an easy question when the constraint is a simple object type like { "foo": string }. But I don't see a tractable path forward in the general case. Consider other constraints you could write:

type WithoutFooGeneric<P extends Partial<Record<"foo" | "bar", string>>> = Omit2<P, "foo">;

This should be an error, since writing

type A = WithoutFooGeneric<Record<"bar", string>>

violates the constraint of Omit2

But how is this error even found? It's not through instantiation of the constraint; that won't work - that fails to catch the error here

// k: "foo" | "bar"
type K = Partial<Record<"foo" | "bar", string>>;

Given an arbitrary constraint T extends C, there are very few things that can be readily determined about any possible instantiation of T -- you can't just use C as-is and get correct answers out of the other side, as shown by the Partial example. We have some logic in place when relating T as a subtype, but once you take a contravariant operation like keyof T, all bets are off.

So TL;DR this isn't the kind of thing someone can just put up a bugfix for - it implies a very large new kind of reasoning to be able to fix beyond the specific case presented here (aside: fixing only specific cases usually makes everything much worse later).

@RyanCavanaugh RyanCavanaugh added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Bug A bug in TypeScript labels Mar 14, 2022
@RyanCavanaugh RyanCavanaugh removed this from the Backlog milestone Mar 14, 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

Successfully merging a pull request may close this issue.