Как создать обобщенные c динамические c типы, соответствующие структуре ввода? - PullRequest
0 голосов
/ 27 февраля 2020

Я строю функцию проверки схемы объекта и пытаюсь сделать тип возвращаемого значения, чтобы динамически соответствовать структуре входного параметра.

// ----- Types
interface SchemaString {
    type?: 'string',
    required?: boolean,
    default?: string
}

interface SchemaNumber {
    type?: 'number',
    required?: boolean,
    default?: number
}

interface SchemaObject {
    type?: 'object',
    required?: boolean,
    default?: {}
    children?: Schema
}

type Schema = {[key:string]: SchemaString | SchemaNumber | SchemaObject};

// ----- Example

// Stuff happens here
function check<T extends Schema>(schema: T): Magic_Type<T>{
   // Processing
}

const schema: Schema = {
    foo: {
        type: 'string'
    }
    bar: {
        type: 'object',
        children: {
            baz: {type: 'string'}
        }
    }
};

const result = check(schema);

// 'result' type should be:
//
// {
//     foo: string
//     bar: {
//         baz: string
//     }
// }
//

Основная цель - получить IDE чтобы выполнить правильное автозаполнение, в зависимости от структуры входа (и избегать {[key:string]: unknown}):

autocompletion demo

Я пробовал трансформатор следующего типа , но это не работает, как задумано:

type Magic_Type<T extends Schema> = {
    [P in keyof T]: typeof T[P]['default']
}

Спасибо за ваше время!

1 Ответ

1 голос
/ 27 февраля 2020

Есть несколько вещей, которые мешают вам получить правильный результат. Во-первых, когда вы делаете const schema: Schema = { ... }, вы фактически теряете информацию о типе буквенных строк. Решение состоит в том, чтобы удалить аннотацию : Schema (поскольку она теряет информацию о типах) и использовать as const для литерала, или использовать специальную функцию идентификации, которая должна захватывать литеральные типы.

Следующее, что вы необходимо сделать свойство type обязательным для вашей схемы, в противном случае все свойства необязательны, чтобы любой тип соответствовал схеме, поэтому вы не можете выполнить сопоставление.

Наконец, свойство "default" не имеет иметь правильный тип для объектов, поэтому вам нужно вместо этого использовать условные типы. Собрав все воедино, вы получите:

// ----- Types
interface SchemaString {
    type: 'string',
    required?: boolean,
    default?: string
}

interface SchemaNumber {
    type: 'number',
    required?: boolean,
    default?: number
}

interface SchemaObject {
    type: 'object',
    required?: boolean,
    default?: {}
    children?: Schema
}

type SchemaItem = SchemaString | SchemaNumber | SchemaObject;
type Schema = {[key:string]: SchemaItem };


type SchemaObjectChildrenToType<T extends Schema | undefined> =
    T extends Schema ? SchemaToType<T> : {};

type SchemaItemToType<T extends SchemaItem> =
    T extends SchemaString ? string :
    T extends SchemaNumber ? number :
    T extends SchemaObject ? SchemaObjectChildrenToType<T["children"]> :
    never

type SchemaToType<T extends Schema> =
    { [P in keyof T]: SchemaItemToType<T[P]> };


// Stuff happens here
function check<T extends Schema>(schema: T): SchemaToType<T>{
   // Processing
    return null as any;
}

function makeSchema<T extends Schema>(schema: T): T {
    return schema;
}

const schema = {
    foo: {
        type: 'string'
    },
    bar: {
        type: 'object',
        children: {
            baz: { type: 'string' }
        }
    }
} as const;

// const schemaAlternative = makeSchema({
//     foo: {
//         type: 'string'
//     },
//     bar: {
//         type: 'object',
//         children: {
//             baz: { type: 'string' }
//         }
//     }
// });

const result = check(schema);

result.bar.baz.endsWith("bar"); // works!

// 'result' type should be:
//
// {
//     foo: string
//     bar: {
//         baz: string
//     }
// }
//

...