3

I have created a function that does the runtime requirement of checking for null and undefined not being true:

 function hasFields<T>(obj: T | T[], ...fields: (keyof T)[]): boolean {
    const inObj: { (obj: T): boolean } = (obj) => fields.every((f) => obj[f] != null);

    if (Array.isArray(obj)) {
        return obj.every((o) => inObj(o));
    } else {
        return inObj(obj);
    }
}

But what I would really like is something that would either return an obj with an updated type, or being able to use this in an if statement and be able to get the types updated within the context of the if statement.

I have seen questions like this: Typescript type RequireSome<T, K extends keyof T> removing undefined AND null from properties but it doesn't do it for a list of fields.

If it helps, the fields are known at compile time.

Jeffrey Drake
  • 805
  • 10
  • 26

1 Answers1

6

The type RequireAndNotNullSome is usable as is for your use case as well. To convert hasFields to a type guard you need to add a type parameter (lets call it K) to capture the actual keys the function was called with. Also to get it to work for both arrays and object types, you will need separate overloads for those casses:



type RequiredAndNotNull<T> = {
    [P in keyof T]-?: Exclude<T[P], null | undefined>
}

type RequireAndNotNullSome<T, K extends keyof T> = 
  RequiredAndNotNull<Pick<T, K>> & Omit<T, K>

function hasFields<T, K extends keyof T>(obj: Array<T | RequireAndNotNullSome<T, K>>, ...fields: K[]): obj is Array<RequireAndNotNullSome<T, K>>
function hasFields<T, K extends keyof T>(obj: T | RequireAndNotNullSome<T, K> , ...fields: K[]): obj is RequireAndNotNullSome<T, K>
function hasFields<T, K extends keyof T>(obj: T | Array<T>, ...fields: K[]): boolean{
    const inObj: { (obj: T | RequireAndNotNullSome<T, K>): boolean } = (obj) => fields.every((f) => obj[f] != null);

    if (Array.isArray(obj)) {
        return obj.every((o) => inObj(o));
    } else {
        return inObj(obj);
    }
}

type Question = {
    id: string;
    answer?: string | null;
    thirdProp?: number | null;
    fourthProp?: number | null;
}

declare var v: Question;

if (hasFields(v, "answer", "thirdProp")) {
  v.answer.anchor // string 
}

declare var arr: Question[];

if (hasFields(arr, "answer", "thirdProp")) {
  arr[0].answer.anchor // string 
}

Playground Link

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 1
    This is really good, thanks! I have to admit I understand C++ Template Metaprogramming better than this stuff. So I am going to have to take a hard look at this wizardry. – Jeffrey Drake May 14 '20 at 19:44