Dmitriy Rudnik
Dmitriy Rudnik

Reputation: 59

Optional keys + keys which exist in `initialData`

Which way to set type of data to get all keys which exist in initialData + other keys from Item as Partial(optional)?

class TrackedInstance<Item extends Record<string, any>, InitialData extends Partial<Item> = Partial<Item>> {
  private _data: Partial<Item> = {}

  constructor(initialData: InitialData, isNew?: boolean) {
    this._data = initialData
  }

  get data() {
    return this._data
  }
}

interface User {
  name: string
  age: number
  isAdmin: boolean
}

const userInstance = new TrackedInstance<User>({
  name: ''
})

const test1: string = userInstance.data.name // Should ok
const test2: number = userInstance.data.age // Should error because age can be 'undefined' | 'number'

Ts playground

I think it will looks something like get data(): Pick<Item, keyof InitialData> & Partial<Omit<Item, keyof InitialData>> {

But it throws error:

TS2344: Type 'keyof InitialData' does not satisfy the constraint 'keyof Item'.

Upvotes: 0

Views: 68

Answers (1)

Kelvin Schoofs
Kelvin Schoofs

Reputation: 8718

I'm not entirely sure whether this is possible without resorting to multiple functions.

One problem with TypeScript generics is that if you specify one generic, you have to specify all of them. With generics with default values, this actually means that once you specify one generic parameter, it will force the others to their default value. Therefore, TrackedInstance<User> forces it to be TrackedInstance<User, Partial<User>>.

User jcalz mentioned this issue in the comments regarding having generic parameter interference for non-specified parameters.

Like I said, this might not be possible without multiple functions to bypass that "default generic value" issue, so here is a solution by using multiple function calls:

class TrackedInstance<Item extends Record<string, any>, InitialData extends Partial<Item> = Partial<Item>> {
    private _data: InitialData;

    constructor(initialData: InitialData, isNew?: boolean) {
        this._data = initialData
    }

    get data(): InitialData {
        return this._data
    }
}

interface User {
    name: string
    age: number
    isAdmin: boolean
}

function track<T>() {
    return function <D extends Partial<T>>(initialData: D, isNew?: boolean) {
        return new TrackedInstance<T, D>(initialData, isNew);
    };
}

const userInstance = track<User>()({
    name: ''
})

const test1: string = userInstance.data.name // Should ok
const test2: number = userInstance.data.age // Should error because age can be 'undefined' | 'number'

Mind that I also had to change your _data type to be InitialData instead of just Partial<Item>. The above code results in the following intellisense/error:

Example intellisense

Here .data is only InitialData. If you also want "partial access" to the rest of the data, make your _data private field be of type InitialData & Partial<Item>.

Upvotes: 2

Related Questions