Closed danvk closed 6 months ago
@infacto Actually we can make first example work with current state of things using some more complex typeguards Playground
type MyObj = { data?: string };
type MyArray = { list?: MyObj[] }[];
const myArray: MyArray = [];
const isNotNullish = <T>(input: T | null | undefined): input is T => input != null
const isNotEmpty = <T>(input: T[]) => input.length !== 0;
function both<I, O1 extends I>(
predicate1: (input: I) => boolean,
predicate2: (input: I) => input is O1,
): (input: I) => input is O1;
function both<I, O1 extends I>(
predicate1: (input: I) => input is O1,
predicate2: (input: O1) => boolean,
): (input: I) => input is O1;
function both<I, O1 extends I, O2 extends O1>(
predicate1: (input: I) => input is O1,
predicate2: (input: O1) => input is O2,
): (input: I) => input is O2;
function both<I>(
predicate1: (input: I) => boolean,
predicate2: (input: I) => boolean,
) {
return (input: I) => predicate1(input) && predicate2(input)
}
type HasNonNullishPredicate<P extends string> = <T extends { [K in P]?: unknown }>(obj: T) => obj is T & { [K in P]: Exclude<T[K], undefined | null> };
function hasNonNullish<P extends string>(prop: P): HasNonNullishPredicate<P>;
function hasNonNullish(prop: string) {
return (input: object) => prop in input && (input as any).prop != null; // this `as any` cast will be unnecessary in TSv4.9
}
const result = myArray
.map((arr) => arr.list)
.filter(both(isNotNullish, isNotEmpty))
.map((arr) => arr
.filter(hasNonNullish('data'))
.map(obj => JSON.parse(obj.data))
);
@temoncher Thanks for your input. Helpful stuff. Somehow I hadn't thought of the return value of the arrow function in the filter method. 🤦♂️ Only indirect. Yes, makes sense. ^^ But anyway, it would be great if TypeScript could just infer that for less and cleaner code.
Sorry if I disturb you. I'm facing this problem again, where infer type guard for filter function would be awesome. Just another contribution post or thinking out loud.
To the example above:
context.filter((obj): obj is Required<MyObj> => !!obj?.data)
This guards the objects to have all properties required. In this example, it's ok. But for more complex object with several (optional) properties should rather do that.
context.filter((obj): obj is MyObj & Required<Pick<MyObj, 'data'>> => !!obj?.data)
Pick only the tested properties. Otherwise you may obfuscate nullish properties. It's a really long johnny. Infer type would simply look like:
context.filter((obj) => !!obj?.data)
Now I craft many type guards to avoid such long complex type defs. I mean this is just a simple example. The real world code type def would be 2 miles of code horizontal scoll. I thinking of a chain guard-type function like rxjs pipes. But it's either exhausting or unsafe (e.g. simple cast from unknown by assuming types). I don't want to rule out that I'm thinking too complicated and that there is a much simpler way. I just want to fix strict type issues. In best case without blind casting.
@infacto in pretty sure that's what isPresentKey does from ts-is-present
This is pretty annoying issue that happens quite often. Can we raise priority on this one?
Leaving https://github.com/total-typescript/ts-reset/ here as a drop-in "fix" until this lands in TS itself.
i hope anyone advocating for using is
knows that's still inherently type unsafe
function isPresent<T>(input: null | undefined | T): input is T {
return !input;
}
const arr = ["string", 8, null, undefined].filter(isPresent);
// expects ["string", 8]
// but is instead [null, undefined]
console.log(arr)
is
is no different from as
, so i still like using flatMap
, especially with the benchmark in https://github.com/microsoft/TypeScript/issues/16069#issuecomment-899633677 revealing that (the JIT compiler is allowing that) the performance cost is smaller than linear
Hi all,
When will TypeScript's type inference be fixed to handle this simple example ?
// Types as `(o: string | undefined) => o is string`
const myGuard = (o: string | undefined): o is string => !o;
// Types as `(o: string | undefined) => boolean` but should type as `(o: string | undefined) => o is string`
const mySecondGuard = (o: string | undefined) => myGuard(o);
Thank you @danvk and @RyanCavanaugh!
@ptitjes The issue with your example is that the first guard is not correct:
const myGuard = (o: string | undefined): o is string => !o;
const emptyString = '';
if(!myGuard(emptyString)) {
// according to the "if and only if" nature of type predicates,
// the fact that the predicate failed means that `emptyString`
// _must not_ be a string, but actually it is.
}
i use such a patch:
diff --git a/node_modules/typescript/lib/lib.es5.d.ts b/node_modules/typescript/lib/lib.es5.d.ts
index a88d3b6..f091ecb 100644
--- a/node_modules/typescript/lib/lib.es5.d.ts
+++ b/node_modules/typescript/lib/lib.es5.d.ts
@@ -1268,6 +1268,7 @@ interface ReadonlyArray<T> {
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): S[];
+ filter<F extends BooleanConstructor>(predicate: F, thisArg?: any): Exclude<T, false | null | undefined | '' | 0>[];
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
@@ -1459,6 +1460,7 @@ interface Array<T> {
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
+ filter<F extends BooleanConstructor>(predicate: F, thisArg?: any): Exclude<T, false | null | undefined | '' | 0>[];
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
with
[].filter(Boolean)
TypeScript Version: 2.3
Code
with
strictNullChecks
enabled.Expected behavior:
This should type check. The type of
evenSquares
should benumber[]
.Actual behavior:
The type of
evenSquares
is deduced as(number|null)[]
, despite the null values being removed via the call to.filter
.