-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Typeguard for union not working after adding a property to the superclass of one of the union members #35953
Comments
@bradzacher: you might be interested in seeing this. I don't think there's much @AnyhowStep: thanks for the help distilling this into the playground reproduction, and helping double-check my sanity :) I think the reason that |
Did you mean interface Alpha {
kind: "A"
}
interface Beta {
kind: "B"
}
interface MyAlpha extends Alpha {
// Add nothing
}
interface MyBeta extends Beta {
// New optional member
y?: any;
}
declare function isMine(arg: any): arg is MyAlpha | MyBeta;
function fn(arg: Alpha | Beta) {
if (isMine(arg)) {
// TS says this is "A"
arg.kind;
}
} What's going on here? TypeScript says that if you have a UDTG and apply it to some union, then the primary intent of that is to probably to do some filtering operation like this: interface Alive { a: any }
interface Plant extends Alive { p: any }
interface Animal extends Alive { m: any }
interface Cat extends Animal { c: any }
interface House { h: any }
declare function isAlive(x: any): x is Alive;
function something(obj: Animal | Plant | House) {
if (isAlive(obj)) {
obj; // obj: Animal | Plant
}
} If the checked-for type is a subtype of the output of that process, we also narrow to exactly that: declare function isCat(x: any): x is Cat;
function something(obj: Animal | Plant | House) {
if (isCat(obj)) {
obj; // obj: Cat
}
} Note that I said "subtype", not "assignable" to. In the OP example, So in your case, TypeScript saw a list of types in a union, filtered them to those that were subtypes, saw that at least one fit, and then didn't bother checking for assignability of the remaining constituents of the union: interface Plant { kind: "plant", names: string[] }
interface Animal { kind: "animal", age: number }
// Subtyping relation to Animal
interface Cat extends Animal { age: number }
// Assignability relation to Plant
interface Tree extends Plant { names: [string] }
declare function isCatOrTree(x: any): x is Cat | Tree;
function something(obj: Animal | Plant) {
if (isCatOrTree(obj)) {
// obj: Animal
obj;
}
} This is an important bit of logic, because many things are an assignability target but not a subtype target. For example, if you just hack up the compiler to always use the assignability relation, you can easily introduce new errors: interface OptionsBag {
a?: string;
b?: string;
c?: string;
name: string;
}
interface Thing {
name: string;
other?: string;
}
declare function isThing(x: any): x is Thing;
function something(arg: Thing | OptionsBag) {
if (isThing(arg)) {
// doesn't narrow 'arg', error here
// because 'other' doesn't exist on OptionsBag
arg.other;
}
} This has been extremely delicately tweaked over the years to try to produce the most intuitive behavior in other cases, so I'm not sure what the right answer is at this point. Any change is going to be a breaking change for other code. For your specific case, I think the only great answer would either be a type assertion, or to fine-tune your interfaces such that they uniformly do or don't have different assignability/subtyping relationships to their parent types. |
@ahejlsberg any additional thoughts? I don't see much room for improvement, unfortunately |
@RyanCavanaugh thanks for such a fantastic write up - I'd be lying if I claimed to understand it completely, but I think I got the bulk of it well enough. Sadly, the tuple member was intentional, but not strictly required as is: I'm trying to represent that we know for sure the value of the first element in that tuple; if there was a way to represent "array of strings, of which the first value is (btw in case it matters, the actual type of the elements in the tuple is I'm not sure I understand why the removing of Regardless, thanks again for the detailed response - I'll have to have a think about the best way to tackle in our codebase :) Just as a toss away thought: I guess a solution could be useful to have some form of as-typeguard a la |
It's moments like this I realise I know a lot less about typescript than I think I do...
type TMyTuple<TFirst extends string> = [TFirst, ...string[]]
// errors
const bad1: TMyTuple<'something'> = []; // Property '0' is missing in type '[]' but required in type 'TMyTuple<"something">'.
const bad2: TMyTuple<'something'> = ['something', 1]; // Property '1' is incompatible with rest element type. Type 'number' is not assignable to type 'string'.
// working
const good1: TMyTuple<'something'> = ['something'];
const good2: TMyTuple<'something'> = ['something', 'a', 'b'];
const first = good1[0]; // typeof === 'something'
const second = good1[1]; // typeof === string |
I feel very silly, b/c I tried this quickly but missed out the Thanks for not letting me get away with that :) Sadly, as I suspected |
@RyanCavanaugh If I understand what I'm reading,
|
Just answering some of my questions, https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types
https://www.typescriptlang.org/docs/handbook/type-compatibility.html#subtype-vs-assignment
|
This issue appeared in
eslint-plugin-jest
after updating@typescript-eslint
to 2.13.0 (from 2.12.0).TypeScript Version: 3.7.x-dev.201xxxxx
Search Terms:
typeguard, union
Code
This is a real life reproduction - it can be made smaller by removing the generics,
BaseNode
, etc, but then it becomes counterproductive as you can argue the solution to be "just useStringLiteral
directly".Expected behavior:
No errors
Actual behavior:
if (argument.type === AST_NODE_TYPES.TemplateLiteral) {
is marked as an error, as TS believesargument.type
can only beAST_NODE_TYPES.Literal
(aka it appears to discard the alternative in the union).This behaviour is tied to the existence of the
value: string
property onStringLiteral
: if you remove that property, everything works as expected (which is what changed between 2.12.0 & 2.13.0: previouslyLiteral
was just an interface w/type
- now it's a union of<Type>Literal
interfaces; one for each of the values ofLiteralBase#value
)Playground Link: https://www.typescriptlang.org/play/?ts=3.8.0-dev.20191231&ssl=1&ssc=1&pln=60&pc=2#code/KYDwDg9gTgLgBAE2AYwDYEMrDsAdgVwFs4BBAZQBUB9AOQHkARAUSooE0AFJsuAbwFgAUHDgAZAJYxgUdKjgBeOAHIJUmaiUAaISIrBCYDFNXTZC5XoNHgJ9VqEBfIUNCRYcGAE8w2CmRoQSOZkMFDiuADmtmYAPnCWhujGkqaoANzOgq7Q8OFqAGboyNgAQugAzsABQQLCcOUw+Pn5AFxwBKjpji7gOXB50oXFYinqZZU4IFK4COVw41WB2LUiMgDubQ1hkRl1AG6y+MBtAEYQEKjA6LhwcQSEJ9K3cABKwBFM4M9b4RHPHV06lgIqAAPxtFYiOBgJJqXCbUK-XZQuD5DARcoI7YRZFwBy7JyCHpuXK4ApFbAhbHROSgaazEZqWQLPg6DzeY6kSi0RgsdhcMgAOhpuIOqCOWKR3SyvXcAygQ18+kSySZtKmeAZC2qyzZXh8bXI1HozFYnG4goS1hFbIAjvgKuJMfVEZEANoAXQJmXlirgAFlPFTfjSADwANUO2DpmrmP0i5njEQAfJN6XNg5Eaaz9lG2pHxcBvUTBL6KQHPFakjZRrII1G07GXdjE66U2yYzM5lXVakcyJ7Y7nW6C0cvdKhPrsIHMxEdaGeJ2GUnW9jU-I2XEZ22w2Rk5uKz2a2qF8ndkIkGhMNh8vhcMgYOIIDcndvsfPw42u83fsmABRsrgSxtH4OraHU5Q+Mg4j5OIyDlOCcDhuBACUbRAUEToVrOH5npkt73o+z6oucf6YBERB4DAIH+EsKH9v0+RwH+r5Bm2OpkVAFGEFRKH0ZCIgAPQAFRsiiIifFBUgIG05GUWSgpTv0cw0jER40mJ4kkA+DpyHAslcfJMCKRyymMqkmTiVCFAABZYcgz4IJIT43Gs4idHAshrOgnhzFgjRQDcSiFKglRKPU4TDDANnYFO5SaSiShGjypr8haNLhdcCBwAlUJJdyJp8uaQrqbWGhwDZ6B7NgQFwBA1VQBgYCCppwmCZpMHMXJPEKUp8j9Vyxq8maAqWsq1plfxmmEiIhIOEAA
Related Issues:
This is probably a duplicate of #31156, but I've created a new issue as I believe this is a more subtle (ideally fixable) issue
The text was updated successfully, but these errors were encountered: