-
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
Better propagation of type predicate signatures #12798
Comments
There actually is a way, but it's cumbersome.
|
Is there a way that this can be improved? It seems very counterintuitive for the type of the predicate to be lost when passed as a callback. |
I'd like to think so, but there needs to be a proposal. I'd like to think that for the following: declare function isNumber(x: any): x is number;
declare function isString(x: any): x is string;
function isSomething(x: any) {
return isNumber(x) || isString(x);
}
But for the following declare function isNumber(x: any): x is number;
declare function isString(x: any): x is string;
function uhhhh(x: any, y: any) {
return isNumber(x) || isString(y);
} we'd like to potentially maintain the checks from each predicate, but we don't have a way to encode those guarantees on the signature of |
What I dont understand is that why 10% of corner cases prevent 80% of straightforward cases from being implemented. Can we error in these cases you mentioned? Or can we default to the current fall back to conventional boolean as it currently is? |
It seems like the return type of x is number & y is any | x is any & y is string But while this could be useful maybe it doesn't need to block inference that doesn't require new syntax to express explicitly. It seems like that would be an orthogonal feature, something like "Correlated User-Defined type Gards". I may be misunderstanding the issue however. |
@Aleksey-Bykov Implementing features that are useful but not entirely thorough attracts a lot of criticism and is oftentimes even questionable. e.g. #13002 |
Exactly, since we dont get to choose, the type guards don't work at certain positions, let's make them more thorough is my message here, and lets make readonly right by breaking some crappy code (#6532 (comment)) that is standing on the way, that sarcasm me gets too. |
I think it is just unexpected. I think it is surprising because type predicates are as rich and compositional as they already if (true || isNumber(x) && isString(x) && isFunction(x)) {
x // {}
}
if (false || isNumber(x) && isString(x) && isFunction(x)) {
x // number & string & Function
} so one may be surprised when they do not work if (!true || isNumber(x) && isString(x) && isFunction(x)) {
x // {}
}
if (!false || isNumber(x) && isString(x) && isFunction(x)) {
x // {}
} these are rather academic however. |
I would like to add that As for With this said there seem not to be a real situation that would require On the other hand the orifinal situation with type guards used for filtering is very real and needs some attention |
Situations that imply
|
@Aleksey-Bykov I agree that these are edge cases. I also like the chip them off one type at a time approach and use it myself. |
Edit: I got confused here between assertions on values and predicates between types, for some reason, please ignore this comment.
Common usage suggest the latter (in the sense that subclasses would pass
|
the type predicate type annotation doesn't operate on two types it operates on a value and a type. A basic type predicate has the type (x: any) => x is T which means that if it returns true, the argument What do you mean by nominal match?
Type predicates take a value and a TypeScript (i.e. erased) type. The |
I 100% agree that the Now that I sort out the confusion Edit: Maybe it was the title that used "type predicates", which probably intended to mean "value type guards", that got me confused. |
Bumping off my "stale" list because this is still a good idea |
Another use case here that seems like it should work but doesn't - direct type assertions in a predicate (as opposed to a type assertion being done by a subroutine of a predicate): class Extension { }
class ImageExtension extends Extension {
imageUrl: string | null = null;
}
const extensions: Extension[] = [];
extensions.push(new ImageExtension());
// Doesn't work
const imageExtension: ImageExtension | undefined = extensions.find(e => e instanceof ImageExtension);
// Does work, but is unnecessarily verbose
const imageExtension2: ImageExtension | undefined = extensions.find((e): e is ImageExtension => e instanceof ImageExtension); |
This may not be the right place to raise this, and I can't guarantee that this is a limitation in the language and not just me coding something poorly.. but given the following type predicate: export const isDefined = <T>(
value: T | null | undefined
): value is NonNullable<T> => value !== null && value !== undefined; It seems that I can't do something like the following, as it seems not to narrow the type (minimal contrived example based on an issue I just hit in my code): const doSomethingWithFoo = (foo?: string) {
const hasFoo = isDefined(foo);
const someObject = { bar: "bar" };
if (hasFoo && someObject.hasOwnProperty(foo) {
// TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string | number | symbol'.
doSomething();
}
} Screenshot of the actual error case in my IDE: With the variables defined as: const hasSprites = isDefined(sprites);
const wantsSprites = isDefined(spriteName); Yet when I use the type predicate functions directly within the Is this another case of TypeScript not 'passing on' it's narrowed types? Or am I just making some kind of 'rookie error' here? |
This would be really great because currently it forces me to sacrifice the type safety. E.g. I have the following predicate: type Checked = ...
function check(smth, x: Blah): x is Blah & Checked { ... } I'd like to filter by this predicate: const filtered: (Blah & Checked)[] =
xs.filter(x => check(smth, x)) For this to work, I have to add a type signature: const filtered: (Blah & Checked)[] =
xs.filter((x): x is typeof x & Checked => check(smth, x)) However, TypeScript doesn't seem to check that const filtered: (Blah & Checked)[] =
xs.filter((x): x is typeof x & Checked => true /* or anything equally wrong */) So each of these lambda type guards is an extra line to potentially worry about. |
This issue makes (TS 5.0) decorators more difficult to write if they want to be able to be applied to potentially type predicates. For example suppose we have: function deprecatedMethod(reason: string) {
function decorator<This, Args extends any[], Return>(
method: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext,
): (this: This, ...args: Args) => Return {
return function decoratedMethod(this: This, ...args: Args): Return {
// A proper implementation would only print once ever, rather than per call
console.warn(reason);
return method.call(this, ...args);
};
}
return decorator;
}
class SomeClass {
@deprecatedMethod(`SomeClass.isSomeClass is deprecated and will be replaced in future`)
static isSomeClass(value: any): value is SomeClass {
return "field" in value;
}
field = "blah";
} Then we get the same problem as in the OP:
While this can be resolved by decorator authors by adding an overload: function deprecatedMethod(reason: string) {
function decorator<This, Kind>(
method: (this: This, value: any) => value is Kind,
context: ClassMethodDecoratorContext,
): (this: This, value: any) => value is Kind;
function decorator<This, Args extends any[], Return>(
method: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext,
): (this: This, ...args: Args) => Return {
return function decoratedMethod(this: This, ...args: Args): Return {
// A proper implementation would only print once ever, rather than per call
console.warn(reason);
return method.call(this, ...args);
};
}
return decorator;
} It feels like it will be a large footgun for authors of fairly generic decorators (like I'm not sure if this could be more narrowly fixed for decorators, or if it would require a general solution like is proposed in the OP. |
The text was updated successfully, but these errors were encountered: