镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Constraint failing for subtype (regression) #29464

Closed

Description

TypeScript Version:
Failing in 3.3.0-dev.20190117

Last version 3.1.6 passing the test

Search Terms:
satisfy constraint subtype FilterFlags

Code

function func1<Base extends { [key: string]: any }, Type extends Base[Key], Key extends keyof Base>(
  attrName: Key,
  attrType: Type
) {
  console.log("func1");
}

function func1String<Base extends { [key: string]: any }, Key extends keyof Base>(attrName: Key) {
  func1<Base, string, Key>(attrName, "string");
}

Expected behavior:
Compilation should pass (like it does on 3.1.6)

Actual behavior:
Compilation fails

test_ts.ts:18:15 - error TS2344: Type 'string' does not satisfy the constraint 'Base[Key]'.

18   func1<Base, string, Key>(attrName, "string");
                 ~~~~~~

Playground Link:
http://www.typescriptlang.org/play/#src=function%20func1%3CBase%20extends%20%7B%20%5Bkey%3A%20string%5D%3A%20any%20%7D%2C%20Type%20extends%20Base%5BKey%5D%2C%20Key%20extends%20keyof%20Base%3E(attrName%3A%20Key%2C%20attrType%3A%20Type)%20%7B%0D%0A%20%20%20%20console.log('func1')%0D%0A%7D%0D%0A%0D%0A%0D%0Afunction%20func1String%3CBase%20extends%20%7B%20%5Bkey%3A%20string%5D%3A%20any%20%7D%2C%20Key%20extends%20keyof%20Base%3E(attrName%3A%20Key)%20%7B%0D%0A%20%20%20%20func1%3CBase%2C%20string%2C%20Key%3E(attrName%2C%20%22string%22)%3B%0D%0A%7D

Related Issues:

Activity

  1. jack-williams commented on Jan 17, 2019

    @jack-williams
    Collaborator

    The access here is unsound; I think it was fixed by #27490.

    Consider the instantiation:

    type Base = { x: "x" };
    type Key = "x";
    type Access = Base[Key]; // "x"
    func1String<Base, Key>("x");

    Here the type of Base[Key] would be "x", which string is not assignable to.

  2. crystalin commented on Jan 17, 2019

    @crystalin
    Author

    What you say makes sense but then, should changing the type of the properties of Base to string should work (or is "extends" preventing it to work) ?

    function func1<Base extends { [key: string]: string }, Type extends Base[Key], Key extends keyof Base>(attrName: Key, attrType: Type) {
        console.log('func1')
    }
    
    function func1String<Base extends { [key: string]: string }, Key extends keyof Base>(attrName: Key) {
        func1<Base, string, Key>(attrName, "string");
    }
    

    Also why in that case, this example work?

    
    interface myInterface1 {
        X: string;
        Y: number;
    }
    
    function func2String<Key extends keyof myInterface1>(attrName: Key) {
        func1<myInterface1, string, keyof myInterface1>(attrName, "string");
    }
    

    This could clearly have "Key" being "Y" and so the type would not be "string" but "number"

  3. jack-williams commented on Jan 17, 2019

    @jack-williams
    Collaborator

    Changing the properties to string will not work; you're intuition about the extends is correct.

    The constraint extends { [key: string]: string } says that whatever type Base gets selected to be, it must be at least as specific as { [key: string]: string }, but it could be more specific. For example, you could instantiate Base with a type that has known properties of literal type, such as:

    type Base = { x: "x" };

    Similarly, because the constraint is only an upper bound, the only thing you can say about Base[Key] is that it is at least as specific as string, but it could be more specific. That's why you cant say for certain that its OK to instantiate it to string, it could be the case that Base[Key] = "x", which would then give an error.

    The reason that your later example works is because you are not instantiating func1 with other generic parameters, rather, concrete types. Here you no longer need to work in bounds, and get determine everything precisely.

    func1<myInterface1, string, keyof myInterface1>

    myInterface1, string, and keyof myInterface1 are all known types. At the call site of func1 you have instantiated Key to be keyof myInterface1, which is equal to "X" | "Y". Whatever you pick for attrName will always satisfy this, because you know that attrName, in the worst case, will have type "X" | "Y". If it is more specific you're still good.

    Basically the difference boils down to where generic parameters appear in an A extends B condition.

    • In the unsound case you have string extends Base[Key], with the generic type Base[Key] on the right hand side
    • In the sound case you have Key extends ("X" | "Y"), with generic type Key appearing on the left hand side.

    In general you can reason about generic parameters on the left hand side of a constraint, but it's hard to reason about generic things on the right hand side.

  4. crystalin commented on Jan 18, 2019

    @crystalin
    Author

    I kind of understand, but I'm not sure how I can replace my solution with something that works on 3.2+
    What I'm trying to achieve is a function that validates attribute of an object at run-time. But to be safer, I also want to validate that the parameters I give to that function matches the object property, at the compilation time:

    This is a more complete example:

    type TypeName<T> = T extends string
      ? "string"
      : T extends number
      ? "number"
      : T extends boolean
      ? "boolean"
      : T extends undefined
      ? "undefined"
      : T extends Function
      ? "function"
      : T extends ArrayLike<any>
      ? "array"
      : T extends Object
      ? "object"
      : never;
    
    type FilterFlags<Base, Condition> = { [Key in keyof Base]: Base[Key] extends Condition ? Key : never };
    type AllowedNames<Base, Condition> = FilterFlags<Base, Condition>[keyof Base];
    
    function validateAttribute<Base extends { [key: string]: any }, Type extends Base[Key], Key extends keyof Base>(
      obj: Base,
      attrName: Key,
      attrType: TypeName<Type>
    ) {
      return typeof obj[attrName] === attrType;
    }
    
    function validateString<Base extends { [key: string]: any }, Key extends AllowedNames<Base, string>>(
      obj: Base,
      attrName: Key
    ) {
      validateAttribute<Base, string, Key>(obj, attrName, "string");
    }
    
    interface myInterface {
      fieldString: string;
      fieldNumber: number;
    }
    
    let myObj: myInterface = {
      fieldString: "hello",
      fieldNumber: 18
    };
    
    validateString(myObj, "fieldString"); // Should pass
    validateAttribute(myObj, "fieldNumber", "number"); // Should pass
    validateString(myObj, "fieldNumber"); // Should fail at compilation because fieldNumber is not a string
    validateAttribute(myObj, "fieldString", "number"); // Should fail at compilation because fieldString is not a number
    validateString(myObj, "unknown"); // Should fail at compilation because unknown doesn't exist
    }
    

    It worked fine with 3.1 but I don't know how to make it on 3.2

  5. jack-williams commented on Jan 18, 2019

    @jack-williams
    Collaborator

    Does this work?

    function validateAttribute<Base extends Record<Key,T>, Key extends keyof Base, T = Base[Key]>(
        obj: Base,
        attrName: Key,
        attrType: TypeName<T>
    ) {
        return typeof obj[attrName] === attrType;
    }
    
    function validateString<Base extends Record<Key, string>, Key extends keyof Base>(
        obj: Base,
        attrName: Key
    ) {
        validateAttribute<Base, Key, string>(obj, attrName, "string");
    }

    Not sure it's the best way though..

  6. crystalin commented on Jan 18, 2019

    @crystalin
    Author

    In this case it works.
    In my actual code I didn't use the Record<key, T> however and kept the Base extends { [key: string]: any }
    However I didn't know we could use the = like that (T = Base[Key]). It fixed my issue

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions