首页 > 解决方案 > Typescript 映射类型,其中仅保留可为空的属性并转换为字符串类型

问题描述

我正在尝试从现有类型创建新的映射类型。我正在寻找用一种string类型替换所有可为空的属性。基本思想是让映射类型用一个类型替换子记录中的所有可为空的属性类型string。如果子记录不包含任何可为空的类型,则子记录本身将从映射类型中排除。此外,任何不是对象的属性都被排除在外。

interface OriginalType {
    foo: {
        bar: string | null;
        baz: number | null;
    };
    bar: {
        qux: string;
        quz: boolean | null;
        wobble: string | null;
    };
    baz: {
        grault: string;
        garply: number;
        flob: boolean;
    };
    version: number;
}

interface ExpectedType {
    foo: {
        bar: string;
        baz: string;
    };
    bar: {
        quz: string;
        wobble: string;
    };
}

到目前为止,我目前已经编写了这个映射类型:

type RetainNullablesAsString<T> = {
    [C in keyof T]: T[C] extends object ? {
        [K in keyof T[C]]: null extends T[C][K] ? string : never;
    }: never;
}

如果测试它,我会得到一个错误:

'{ quz: string; 类型中缺少属性 'qux' 摆动:字符串;}' 但在 '{ qux: never; 类型中是必需的 quz:字符串;摆动:字符串;}'。

我打算将该qux属性从类型中排除,因为它不可为空,但我不知道该怎么做 - 我目前将其设置never为映射类型。我认为 Pick 实用程序类型在这里没有帮助,因为我不想将任何属性硬编码到映射类型。此外,baz应该排除该记录,因为它的所有子属性都不能为空。此外,我不知道是否可以创建这样的映射类型,但如果可以的话,我会很高兴。

interface OriginalType {
    foo: {
        bar: string | null;
        baz: number | null;
    };
    bar: {
        qux: string;
        quz: boolean | null;
        wobble: string | null;
    };
    baz: {
        grault: string;
        garply: number;
        flob: boolean;
    };
    version: number;
}
  
const sourceRecord: OriginalType = {
    foo: {
        bar: 'bar',
        baz: 0,
    },
    bar: {
        qux: 'qux',
        quz: false,
        wobble: null,
    },
    baz: {
        grault: 'grault',
        garply: 1,
        flob: true,
    },
    version: 1,
}

//interface ExpectedType {
//    foo: {
//        bar: string;
//        baz: string;
//    };
//    bar: {
//        quz: string;
//        wobble: string;
//    };
//}

type RetainNullablesAsString<T> = {
    [C in keyof T]: T[C] extends object ? {
        [K in keyof T[C]]: null extends T[C][K] ? string : never;
    }: never;
}

const sourceRecordMetaData: RetainNullablesAsString<OriginalType> = {
    foo: {
        bar: 'bar-meta',
        baz: 'baz-meta',
    },
    bar: {
        quz: 'quz-meta',
        wobble: 'wobble-meta',
    },
}

标签: typescriptnullablemapped-types

解决方案


interface OriginalType {
    foo: {
        bar: string | null;
        baz: number | null;
    };
    bar: {
        qux: string;
        quz: boolean | null;
        wobble: string | null;
    };
    baz: {
        grault: string;
        garply: number;
        flob: boolean;
    };
    version: number;
}

type IsNullableProperty<K extends keyof T, T> = null extends T[K] ? K : never;

// Filtering out non nullable properties and convert rest to strings
type ConvertSubRecord<T> = {
    [K in keyof T as IsNullableProperty<K, T>]: string;
};

type IsEmptyObject<T> = keyof T extends never ? never : T;

// It transform property keys for non records and records that are converted into empty objects into never
type IsNotEmptySubRecord<K extends keyof T, T> = T[K] extends object
    ? keyof ConvertSubRecord<T[K]> extends never
    ? never
    : K
    : never;

type Convert<T> = {
    // Filter out with IsNotEmptySubRecord helper and convert subrecords with ConvertSubRecord
    [K in keyof T as IsNotEmptySubRecord<K, T>]: ConvertSubRecord<T[K]>;
};

interface ExpectedType {
    foo: {
        bar: string;
        baz: string;
    };
    bar: {
        quz: string;
        wobble: string;
    };
}

const res: Convert<OriginalType> = {
    bar: {
        quz: "",
        wobble: ""
    },
    foo: {
        bar: "",
        baz: "",
    },
}

推荐阅读