-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Allow method type guards #11117
Comments
Semi related to a random thought here in that more complicated guards could express codependency of properties: interface Array<T> {
length: number;
pop(): this.length > 0 ? T : undefined;
}
const arr = [ 1, 2, 3 ];
if (arr.length > 0) {
const val = arr.pop(); // val is number, not number | undefined under strictNullChecks
} |
|
Would this also fix issues with Map? const cache = new Map<number, string[]>()
if (cache.has(n)) {
return cache.get(n)! // The if statement above should make return type `string[]` instead of `string[] | undefined`
} |
What about using export class Maybe<T> {
constructor(public value: T | undefined) {}
hasValue(): this is this & { value: T } { return this.value !== undefined; }
}
///////
declare var x: Maybe<string>;
if (x.hasValue()) {
x.value.toLowerCase();
} @mattmazzola I've used the above technique to describe a way that |
Ok, I'll have to learn/explore more about the |
@DanielRosenwasser Any way to make that approach work for private properties? |
I wanted to use this for this pattern:
But it gives me the following error: "Property 'end' has conflicting declarations and is inaccessible in type 'DateInterval & { end: Date; }'.(2546)" |
The problem with the solution by @DanielRosenwasser:
is that "else" doesn't work: if (x.hasValue()) {
x.value.toLowerCase();
}
else {
x.value // x.value is string | undefined instead of undefined
} Is there any way to negate the |
Yep, here's a better version - type Maybe<T> =
& ( Just<T>
| Nothing
)
& MaybePrototype<T>
interface Just<T> { value: T }
interface Nothing { value: undefined }
interface MaybePrototype<T> { isJust(this: Just<T> | Nothing): this is Just<T> }
type MaybeConstructor =
new <T>(value: T | undefined) => Maybe<T>
const Maybe = (class MaybeImpl<T> {
constructor(public value: T | undefined) {}
isJust() { return this.value !== undefined }
}) as MaybeConstructor
const x = new Maybe("");
if (x.isJust()) {
x; // Just<string>
x.value // string
x.value.toLowerCase();
} else {
x; // Nothing
x.value; // undefined
let test: undefined = x.value;
} |
(only for sake of showing some stuff off, don't take it seriously :P) In a language like TypeScript it's better to write abstractions in something like static-land. If at all one wants to consume it in the class form I wrote a little thing called type DateInterval =
| { start: Date, end: Date }
| { start: Date, end: null }
const _DateInterval = {
of: (start: Date, end: Date | null) =>
({ start, end }) as DateInterval,
isOngoing: (t: DateInterval): t is DateInterval & { end: null } =>
t.end === null,
isFinite: (t: DateInterval): t is DateInterval & { end: Date } =>
!_DateInterval.isOngoing(t),
toFiniteString: (t: DateInterval) => {
if (_DateInterval.isFinite(t)) {
return `${t.start.toString()} - ${t.end.toString()}` // no error
}
throw new Error("This DateInterval is ongoing, cannot convert to finite interval string")
}
}
const DateInterval = classFromStaticLand(_DateInterval, "DateInterval");
let x = new DateInterval(new Date(), new Date());
console.log(x)
if (x.isOngoing()) {
x.end // null;
x.end.getDay() // expected error
} else {
x.end // Date;
console.log(x.end.getDay())
} Here's what the console would look like... Here's the playground link to try it out. And here's the full code for referenceconst main = () => {
type DateInterval =
| { start: Date, end: Date }
| { start: Date, end: null }
const _DateInterval = {
of: (start: Date, end: Date | null) =>
({ start, end }) as DateInterval,
isOngoing: (t: DateInterval): t is DateInterval & { end: null } =>
t.end === null,
isFinite: (t: DateInterval): t is DateInterval & { end: Date } =>
!_DateInterval.isOngoing(t),
toFiniteString: (t: DateInterval) => {
if (_DateInterval.isFinite(t)) {
return `${t.start.toString()} - ${t.end.toString()}` // no error
}
throw new Error("This DateInterval is ongoing, cannot convert to finite interval string")
}
}
const DateInterval = classFromStaticLand(_DateInterval, "DateInterval");
let x = new DateInterval(new Date(), new Date());
console.log(x)
if (x.isOngoing()) {
x.end // null;
x.end.getDay() // expected error
} else {
x.end // Date;
console.log(x.end.getDay())
}
}
const classFromStaticLand =
((m: any, name: any) =>
function (this: any, ...a: any[]) {
Object.assign(this, m.of(...a))
Object.setPrototypeOf(
this,
Object.fromEntries(
Object.entries(m)
.filter(([k]) => k !== "of")
.map(([k, f]) => [
k,
function (this: any, ...a: any[]) {
return (f as any)(this, ...a);
}
])
)
)
this[Symbol.toStringTag] = name;
}
) as any as FromStaticLand.Class
main();
namespace FromStaticLand {
export type Class =
<M extends { of: (...a: any[]) => object }>(m: M, name: string) =>
new (...a: M["of"] extends (...a: infer A) => any ? A : never) =>
Instance<M>
type Instance<M extends { of: () => unknown }, T = ReturnType<M["of"]>> =
& T
& { [K in Exclude<keyof M, "of">]:
IsGuard<M[K]> extends true
? Guard<T, M[K]>
: Method<T, M[K]>
}
interface Method<T, F>
{ (this: T, ...a: Parameters<F>): Called<F>
}
interface Guard<T, F>
{ (this: T, ...a: Parameters<F>): this is GaurdApplied<F>
}
type IsGuard<F> = F extends (t: any) => t is any ? true : false
type Parameters<F> = F extends (t: any, ...a: infer A) => any ? A : never;
type Called<F> = F extends (t: any, ...a: any[]) => infer R ? R : never;
type GaurdApplied<F> = F extends (t: any, ...a: any[]) => t is infer U ? U : never;
} But more seriously one way to facilitate this is to allow typing class Maybe<T> {
constructor(this: Just<T> | Nothing, public value: T | undefined) {}
isJust(): this is Just<T> { return this.value !== undefined }
}
interface Just<T> { value: T }
interface Nothing { value: undefined }
if (x.isJust()) {
x; // Just<string>
x.value // string
} else {
x; // Nothing
x.value; // undefined
} The problem is this... type Maybe<T> = { value: T | undefined }
type Test1 = Maybe<string>
type Test1IfBranch = Test1 & { value: string } // { value: string }
type Test1ElseBranch = Exclude<Test1, Test1IfBranch> // { value: string | undefined } (no good)
type MaybeWithThisAnnotated<T> = { value: T } | { value: undefined }
type Test2 = MaybeWithThisAnnotated<string>
type Test2IfBranch = Test2 & { value: string } // { value: string }
type Test2ElseBranch = Exclude<Test2, Test2IfBranch> // { value: undefined } (nice) |
TypeScript Version: 2.0.3
Code
Why I can have function type guard:
but not method type guard:
?
The text was updated successfully, but these errors were encountered: