menfon
menfon

Reputation: 1817

General way of picking nullable properties and making them non-nullable

Let's have a following example type:

interface A {
    a?: number;
    b: string;
}

My goal is to have a generic way of creating following type:

interface ExpectedA {
    a: number;
}

So I want to remove all non-nullable fields (those which can contain null and/or undefined) and make those remaining nullable fields non-nullable.

This is how I imagine it should work:

const expA1: ExpectedA = {}; // should NOT pass
const expA2: ExpectedA = {a: 1}; // should pass
const expA3: ExpectedA = {b: ''}; // should NOT pass
const expA4: ExpectedA = {c: 0}; // should NOT pass
const expA5: ExpectedA = {a: 1, b: ''}; // should NOT pass

This is my non working attempt (annotated in comments what it does and what it should do):

export type ExtractNullable<T> = {
    [K in keyof T]: T[K] extends undefined | null ? NonNullable<T[K]> : never;
};

const a1: ExtractNullable<A> = {}; // should NOT pass, wrong error "prop. b is missing"
const a2: ExtractNullable<A> = {a: 1}; // should pass, wrong - "number not undefined"
const a3: ExtractNullable<A> = {b: ''}; // should NOT pass, wrong - "string not never"
const a4: ExtractNullable<A> = {c: 0}; // should NOT pass, ok - "c not on ..."
const a5: ExtractNullable<A> = {a: 1, b: ''}; // should NOT pass, wrong error "number not undefined, string not never"

I think the problem is in the conditional type, but looking at the docs, I have no idea what to change.

Upvotes: 5

Views: 2403

Answers (2)

Wu Wei
Wu Wei

Reputation: 2277

A simple and nice solution in my case with today's available utilities for me was to create an interface that extends on the Pick, that also includes the nullable (to be made non-nullable) fields. Then on the interface itself declare the fields that should be non-nullable again, basically overwriting it. Like so:

interface Dog
   extends Pick<
      Animal,
      "name" | "birthDate" | "breed"
   > {
   breed: string;
}

It's straightforward and readable.

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

You need to select only the nullable keys first and then map them.

interface A {
    a?: number;
    b: string;
}

export type NullableKeys<T> = {
    [P in keyof T]-? :  Extract<T[P], null | undefined> extends never ? never: P
}[keyof T]
export type ExtractNullable<T> = {
    [P in NullableKeys<T>]: NonNullable<T[P]>
}

const a1: ExtractNullable<A> = {}; // err
const a2: ExtractNullable<A> = {a: 1}; //ok
const a3: ExtractNullable<A> = {b: ''}; // err
const a4: ExtractNullable<A> = {c: 0}; // err
const a5: ExtractNullable<A> = {a: 1, b: ''}; //err

The above approach works with strictNullChecks since the type of optional properties is changed to include undefined. A version that picks optional properties and works without this compiler option is:

export type NullableKeys<T> = {
    [P in keyof T]-?:  Pick<T,P> extends Required<Pick<T, P>> ? never: P
}[keyof T]

Upvotes: 5

Related Questions