In typescript, can I remove the undefined and null types from an object using a list of keys?
Asked Answered
C

1

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.

Chandra answered 12/5, 2020 at 19:43 Comment(0)
H
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

Hollister answered 13/5, 2020 at 4:20 Comment(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.Chandra

© 2022 - 2024 — McMap. All rights reserved.