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

Constraint failing for subtype (regression) #29464

Closed
crystalin opened this issue Jan 17, 2019 · 6 comments
Closed

Constraint failing for subtype (regression) #29464

crystalin opened this issue Jan 17, 2019 · 6 comments

Comments

@crystalin
Copy link

crystalin commented Jan 17, 2019

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:

@jack-williams
Copy link
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.

@crystalin
Copy link
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"

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 17, 2019

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.

@crystalin
Copy link
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

@jack-williams
Copy link
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..

@crystalin
Copy link
Author

crystalin commented Jan 18, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants