简体   繁体   中英

Typescript type inference and optional object property

Basically what I want to achieve is to have class X infer its K type from the provided constructor argument typing.

I have written this:

interface XContstructorParams<K extends string = never> {
    record?: Record<K, number>;
    // Other stuff with some being optionals, which is why it's all in single
    // object, as I don't want to do new X(arg1, undefined, undefined, arg2)
}

class X<K extends string = never> {
    private record: Record<K, number>;
    constructor(params: XContstructorParams<K>) {
        // I'd rather avoid to use a type assertion here but it's not a big deal
        // compared to the issue below.
        this.record = params.record || {} as Record<K, number>;
    }
    
    getNumber(k: K): number {
        return this.record[k];
    }
}

This actually works to infer the type of K to be never when the record property is not given, but it does not prevent to manually specify an incorrect value for K:

// Correctly inferred to be X<'a', 'b'>
const x1 = new X({ record: {'a': 1, 'b': 2} });
// Correctly inferred to be X<never>
const x2 = new X({});
// Oops ! This is incorrect as `getNumber` will not return a number when called with 'c'
// or 'd ! But it is allowed because of how I wrote the constructor `record` property to
// be optional. I want it to be optional only when K is never. It's either you provided
// a record and it's keys determine K, or you didn't give a record and K is never, but
// should not be able to specify K and not provide a record matching your specified K.
const x3 = new X<'c' | 'd'>({});

I'd like x3 to be recognized as a compilation error but I couldn't manage to make it work. What am I doing wrong?

EDIT: for some reason, stack overflow removes my "hello" at the start of the message so... better late than never I guess... Hello and thanks for your help; ;-)

Is this solution works for you?

interface XContstructorParams<K extends string = never> {
  record: K extends never ? undefined : Record<K, number>;
  // Other stuff with some being optionals, which is why it's all in single
  // object, as I don't want to do new X(arg1, undefined, undefined, arg2)
}

class X<K extends string = never> {
  private record: Record<K, number>;
  constructor(params: XContstructorParams<K>) {
    // I don't think you need type castin here
    this.record = params.record
  }

  getNumber(k: K): number {
    return this.record[k];
  }
}


// Correctly inferred to be X<'a', 'b'>
const x1 = new X({ record: { 'a': 1, 'b': 2 } });
const result = x1.getNumber('a') // number

const x2 = new X<never>({}); // error


const x3 = new X<'c' | 'd'>({}); // error

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM