-
Notifications
You must be signed in to change notification settings - Fork 12.8k
tsconfig disable: is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint #36821
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
As an alternative, it could also be nice to be able to define a generic as non-narrowing. For example, using the made up keyword const testFn = <A openly extends string>(a: A = 'foo') => `hello!`;
testFn<"qux" | "baz">("qux"); // Error: generic type "qux" | "baz" is not an open extension of type "string"
const quxBaz: "qux" | "baz" = "qux";
testFn(quxBaz); // Should probably not error; |
Do these examples resemble actual use cases? If so, I'd say that you should either not be using generics or you should just use a type assertion to allow unsafe narrowings: const notGeneric = (a: string = "foo") => "hello";
const asserted = <A extends string>(a = "foo" as A) => "hello"; Maybe you have a more fleshed-out motivating example for why you want this feature? Something that I'd look at and think "hmm, yeah, that clearly should be a generic function and the type assertion is incredibly annoying to use". Probably some object type instead of As for the " |
I really don't think adding a compiler flag to ignore this is the correct solution here. I'm pretty sure your use cases are where you want a generic function where if the argument is not passed the argument and generic both default to some value, like this: function wrap<V extends string = "foo">(val: V = "foo" as V) {
return [val] as [V];
}
const a = wrap("hello"); // typeof a <==> ["hello"]
const b = wrap(); // typeof b <==> ["foo"]
// this should probably not be allowed....
const c = wrap<"hello">(); // typeof c <==> ["hello"] but thats not correct at runtime! The issue with doing function wrap2(): ["foo"];
function wrap2<V extends string>(val: V): [V]
function wrap2(val: string = "foo"){
return [val];
}
// now this isn't allowed yay!
wrap2<"hello">();
declare const value: "hi" | undefined;
// but neither is this since there isn't an overload that accepts undefined :(
wrap2(value); // would we want this to be ["hi" | "foo"] ? So our overload signature didn't capture a case where a variable that might be undefined is passed in, in that case our generic type ReplaceUndefined<T,R> = T extends undefined ? R : T;
function wrap3(): ["foo"];
function wrap3<V extends string | undefined>(val: V): [ReplaceUndefined<V,"foo">]
function wrap3(val: string = "foo"){
return [val];
}
const d = wrap3(value); // typeof d <==> ["hi" | "foo"] This gets absurdly complicated to perfectly represent such a simple case. I am on board with having a possibly less complicated way of handling default values in generic functions but I haven't been able to think of any decent ways. I certainly don't think just ignoring the error is a good idea. |
Okay, here's the case I'm working with: export type TestProxy<TARGET> = (
TARGET extends Array<any> ? TestArray<TARGET> :
TARGET
)
type TestArray<TYPE extends Array<any>> = {
[key in (keyof TYPE & number)]: TYPE[key];
} & {
length, push, concat,
join, reverse, shift, slice,
sort, splice, unshift, indexOf,
lastIndexOf, every, some, forEach,
filter, reduce, reduceRight, find,
findIndex, fill, copyWithin, [Symbol.iterator],
entries, keys, values, [Symbol.unscopables],
includes, flatMap, flat,
// pop,
pop(): TestProxy<TYPE[number]>;
map<OUT>(cb: (it: TYPE[number], index: number, array?: TestArray<TYPE>) => OUT): TestProxy<Array<OUT>>;
}
function proxyMe<TYPE>(target: TYPE): TestProxy<TYPE> {
return target as any; // imagine proxy
}
const hat: string = proxyMe("hat"); // okay
const obj: { arr: number[]} = { arr: []};
obj.arr = proxyMe([1, 2, 3]); // 'TestArray<U>' is assignable to the constraint of type 'U', but 'U' If you switch out the two definitions of So why does making pop return My point with this issue is: I don't know and I'd really rather not be putting the time in right now to figure it out. Perhaps, in the future I'd really like to geek out over exactly what's going on here. . But right now, I've got more than enough unit tests to be happy with my code. I want to skip this lesson and get back to my job. |
Can you edit that code so that when it's dropped in the Playground it demonstrates what you're talking about and only that? Right now this is a lot of red squigglies and the error you're talking about doesn't match, and if I switch to the other |
Updated Code: export type TestProxy<TARGET> = (
TARGET extends Array<any> ? TestArray<TARGET> :
TARGET
)
type TestArray<TYPE extends Array<any>> = {
[key in (keyof TYPE & number)]: TYPE[key];
} & {
length: any, push: any, concat: any,
join: any, reverse: any, shift: any, slice: any,
sort: any, splice: any, unshift: any, indexOf: any,
lastIndexOf: any, every: any, some: any, forEach: any,
filter: any, reduce: any, reduceRight: any, find: any,
findIndex: any, fill: any, copyWithin: any, [Symbol.iterator]: any,
entries: any, keys: any, values: any, [Symbol.unscopables]: any,
includes: any, flatMap: any, flat: any,
// pop: any,
pop(): TestProxy<TYPE[number]>;
map<OUT>(cb: (it: TYPE[number], index: number, array: TestArray<TYPE>) => OUT): TestProxy<Array<OUT>>;
}
function proxyMe<TYPE>(target: TYPE): TestProxy<TYPE> {
return target as any; // imagine proxy
}
const hat: string = proxyMe("hat"); // okay
const obj: { arr: number[] } = { arr: [] };
const testArr = proxyMe([1, 2, 3]);
obj.arr = testArr; // 'TestArray<U>' is assignable to the constraint of type 'U', but 'U' |
Okay so the issue here is the compiler's relative inability to manipulate conditional types that depend on an unspecified generic type parameter. (Someone should be able to find related issues in GitHub for this; assignability of generic conditional types) And the suggestion is to provide a compiler option whereby an assignment to such a generic conditional type is allowed as long as some specification of the generic would make the assignment valid? Like an "optimistic generic narrowing" option? I could see that, I guess. I still feel that type assertions are the right approach in these cases; I'd rather see the compiler get better at dealing with unspecified generics than letting people turn off a whole class of error that is often correctly pointing out a real problem. For the particular code above, if |
I have a very serious question here, pointed at the core beliefs of the TypeScript team: What is the problem with letting users -- of their own free will -- opt out of features that do not benefit them? In terms of your solutions:
What I think makes most sense is allowing users to opt-out of not just this, but literally anything they choose. In a perfect world, the compiler would always be right... I want to see that world too. But nothing is perfect, your compiler included. Sometimes your rules need exceptions, and rather than brick-walling users until new rule is made it's a lot more logical to let users opt-out and hopefully fix the problem after some discussion. Let's be honest here: what's almost certainly going to happen is that you guys are just going to tell me -- indirectly of course -- that I don't matter enough to change things so I can just deal with it. There will be this whole song and dance about progress, but I've seen better ideas from more popular people get nowhere. And yet it would be so easy to make things better: If you guys create an opt-out flow now, it will make it so every single flaw in the compiler is a non-blocker from now until the end of time. |
In case it's not obvious (maybe it is, and if so, sorry), I'm not part of the TypeScript team; I'm just a very loud fan. 🏲 🕫 GO TEAM! 🕫 🏲 I'd be more inclined to support such opt-in/opt-out features if they could be contained to libraries and not infect a whole project. As a developer I wouldn't want to use a tool if it forced me to write a million type assertions, but I also wouldn't want to use it if it forced me to turn off a class of compiler errors that are otherwise useful. 🤷♂️ |
@jcalz, you've been great. You've helped me a ton on SO as well, and I really appreciate it.
I'd expect it to be along the same lines as Where I'm at -- if you can imagine -- is at the tail end of spending a year of free-time creating a recursive state manager which can be used exactly like a standard object in every testable way except for this one single type error that is provably an error in the compiler. Essentially, I have a recursive redux using proxies, with an army of unit tests to make sure it works (because proxies). For now, the best I've got is adding a
If you can think of anything better, let me know. |
Ok I now understand what you are trying to do, sorry my first comment was totally unrelated... I think I can shed some light on what is going wrong with your definitions though. First thing is that the message function demo<T>() {
/*
Type 'string[]' is not assignable to type 'T'.
'string[]' is assignable to the constraint of type 'T',
but 'T' could be instantiated with a different subtype of constraint '{}'.ts(2322)
*/
let x: T = [
"anything goes if you are only validating the implicit constraint of {}",
"btw, EVERYTHING except undefined and null are assignable to {}",
];
} So don't focus on the "assignable to constraint" part, focus on the "not assignable to T" part, that will give you a better indication of what typescript is trying to tell you.
Nope, sorry but your definition actually doesn't support this: type TYPE = Array<string> & { extraField: string };
const thing: TYPE = null as any as TestProxy<TYPE>
/*
Type 'TestArray<TYPE>' is not assignable to type 'TYPE'.
Property 'extraField' is missing in type 'TestArray<TYPE>' but required in type '{ extraField: string; }'.ts(2322)
*/ So when typescript tells you type TestArray<TYPE extends Array<any>> = TYPE & {
pop(): TestProxy<TYPE[number]>;
map<OUT>(cb: (it: TYPE[number], index: number, array: TestArray<TYPE>) => OUT): TestProxy<Array<OUT>>;
} This works, and reduces your type definitions which also reduces error messages when it is invalid in something. |
Okay, I finally now see your point about "What if a user used a special extended array." Hopefully an Your solution does not show the right types: You can see in the image that the type of Is there a way I can enforce that |
This works: export type TestProxy<TARGET> = (
TARGET extends Array<any> ? TestArray<TARGET> :
TARGET
)
type TestArray<TYPE extends Array<any>> = {
[key in (keyof TYPE & number)]: TYPE[key];
}
& Omit<TYPE, keyof Array<any>> // now covering any weird extensions of Array
& {
length: any, push: any, concat: any,
join: any, reverse: any, shift: any, slice: any,
sort: any, splice: any, unshift: any, indexOf: any,
lastIndexOf: any, every: any, some: any, forEach: any,
filter: any, reduce: any, reduceRight: any, find: any,
findIndex: any, fill: any, copyWithin: any, [Symbol.iterator]: any,
entries: any, keys: any, values: any, [Symbol.unscopables]: any,
includes: any, flatMap: any, flat: any,
pop: any,
// pop(): TestProxy<TYPE[number]>;
map<OUT>(cb: (it: TYPE[number], index: number, array: TestArray<TYPE>) => OUT): TestProxy<Array<OUT>>;
}
type TYPE = Array<string> & { extraField: string };
const thing: TYPE = null as any as TestProxy<TYPE>; // no errors here So this means that whether TYPE is an Array or anything else, it will always be equivalent. Once again, why does making |
We did; it's There are many cases where this error is 100.0% correct and stops real unsoundnesses from occurring. There are also cases where this error is due to a limitation in TS's ability to reason about your code. A flag is the wrong solution to "Sometimes this error is correct; sometime it's too conservative" because you should really be thinking about these on a case-by-case basis; it's not the case that a project should unilaterally decide that it wants generics to just be extremely unsound everywhere. |
a broken clock.
This is an opinion. To say something can never be is unprovable. I'm literally living the experience that negates this.
So let me tell it to stop trying. |
At this point it's obvious that "No, fuck me, I don't matter and my problems aren't real," but this is a legit bug, so I'm going to open up a non-feature request issue about it. |
We've declined probably hundreds of commandline flags and none of them for the reason you've assumed. Please don't take things personally. |
I'm okay with "No." But what you're saying is "No, your problem isn't real." You don't have to add that part last part. You could say:
... and I'd take it. Fine. It's your project. But this:
Is provably false and patronizing; as if I don't know what I'm doing. You're allowed to say no. But talking down to me for trying to get around a currently unnecessary, non-beneficial brick-wall is -- rightfully -- offensive. |
Please don't put words in my mouth and then tell me I shouldn't have said them. I'm making benign normative statements here; our experience is that this particular error is frequently encountered in situations where a) the code is objectively wrong and b) the user doesn't realize it yet. I've tweeted about this many times, e.g. because it's so frequently encountered. A flag to disable this error entirely would be akin to a flag that would disable our error message when writing Literally, if I had to pick one error where people say "You shouldn't have given me this error" and the response is most likely to be "No, your code is actually wrong", this would be the one. We have thousands of different errors and approximately five commandline flags for disabling specific generally-correct errors (all of which we regret adding). Our general experience is that it's not a productive solution and that |
Sorry. I meant to say: "It feels like." You are right though, that was inflammatory.
How is dealing with this more fun than watching users ignore your warnings and shoot themselves in the foot? How do you expect users to learn the value if they can't experience its absence? And what if you're wrong? |
I work in the field of leading large scale art pieces, and the amount of ridiculous ideas artists have is over the top. So I tell them what to worry about, and how not to get too burnt and let them go for it. It's something I learned in leadership training, and it's a super effective way to let things you didn't think would work happen while simultaneously protecting your people from huge flaws. If you block a person from experimentation, you'll never get the best out of them. There's some really dope shit I'm trying to do with TypeScript, and it's probably not going to happen because of this error. |
You can probably work around that by reversing the order, so export type TestProxy<TARGET> = TARGET extends Array<any> ? TestArray<TARGET> : TARGET;
interface TestArrayOverrides<T> {
pop(): TestProxy<T> | undefined;
map<OUT>(cb: (it: T, index: number, array: this) => OUT): TestArray<OUT[]>;
/** to ensure that pop() returns correct type */
rock_out(): void;
}
type TestArray<Orig extends Array<any>> = TestArrayOverrides<Orig[number]> & Orig;
function proxyMe<TYPE>(target: TYPE): TestProxy<TYPE> {
return target as any; // imagine proxy
}
const obj: { arr: number[][] } = { arr: [] };
const testArr = proxyMe([[1], [2], [3]]);
const subarr = testArr.pop()!
subarr.rock_out(); // extra field is visible 😄
const normalArr: number[][] = testArr; // also assignable 😄
const normalSub: number[] = subarr; If that doesn't work for you I'm not sure how to get it so |
Uh oh!
There was an error while loading. Please reload this page.
Search Terms
I also searched through (https://www.typescriptlang.org/docs/handbook/compiler-options.html)
Suggestion
Add an
--ignoreSubtypeConstraint
-- but better named -- to make these errors non-blockingUse Cases
In my experience, almost any time this error comes up, it's a bit of a time sink. I appreciate its correctness, but the edge case that it protects against has not yet been helpful for me. By being able to let this type of error slide by, I could strike a better balance between effort and correctness during my work hours.
Examples
This is a great example of something I'd like to configure typescript to overlook:
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: