Задача
Ниже приведен упрощенный пример того, как мы контролируем и передаем данные в приложении. Он используется во многих местах и работает для перевода данных между пользовательским интерфейсом, API и базой данных. API и UI используют camelCase
. База данных использует snake_case
.
В настоящее время это неудобная комбинация типов Partial / Pick, чтобы получить некоторый набор текста, где ...
const item = { fooBar: 'something' }
Item.cast(item).value // returns type Partial<ItemModel>
Item.create(item).value // returns type ItemModel
Цель состоит в том, чтобы вернуть реальный возвращаемый тип объекта .
// Examples
const item = { fooBar: 'something' }
Item.cast(item).value // returns { fooBar: 'something' }
Item.cast(item).databaseFormat // returns { foo_bar: 'something' }
Item.create(item).value // returns { id: '{uuid}', fooBar: 'something' }
Item.create(item).databaseFormat // returns { id: '{uuid}', foo_bar: 'something' }
const itemFromDatabase = { id: '{uuid}', foo_bar: 'something', baz: null }
Item.cast(itemFromDatabase).value // returns { id: '{uuid}', fooBar: 'something', baz: null }
Item.cast(itemFromDatabase).databaseFormat // returns { id: '{uuid}', foo_bar: 'something', baz: null }
Есть идеи по этому поводу? Я бы представил это что-то вроде возвращаемого типа Object.entries (), но я не могу понять правильную комбинацию T keyof
.
// https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript
interface ObjectConstructor {
// ...
entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
// ...
}
Код
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
interface ItemModel {
id: string
fooBar: any
baz?: number
}
interface ItemDatabaseModel {
id: string
foo_bar: any
baz?: number
}
export class Item {
private _data: Partial<ItemModel>
public static cast(item: Partial<ItemModel | ItemDatabaseModel>): Item {
return new this(camelcaseKeys(item))
}
public static create(item: Optional<ItemModel | ItemDatabaseModel, 'id'>): Item {
// Use item.id or add item.id === null to force key
return new this(camelcaseKeys(item))
}
private constructor(input: Partial<ItemModel>) {
// Validate "input" properties have a Item class property setter, else throw
// foreach => this[key] = input[key]
}
get databaseFormat() { return snakecaseKeys(this._data) }
get value() { return this._data }
set id(value: string | null) {
// automatically generate an ID if null, otherwise validate
this._data.id = value
}
set fooBar(value: any) {
// validate
this._data.fooBar = value
}
set baz(value: number | null) {
// validate
this._data.baz = value
}
}