Search code examples
typescriptgenericsextends

Typescript error when using generic T extends Type on container class but not with direct reference to Type


in the following code, if I use T extends HealthyConfig and create a Config class instance using the generic T I get an error of Type 'boolean' does not satisfy the constraint 'T[keyof T]', but when directly referencing HealthyConfig no error is displayed. Surely because T extends HealthyConfig the types should be known as far as at least those defined in HealthyConfig so why doesn't it work with using T?

type Key = string | number;
type Val = Key | boolean | Obj;
type Obj = { [x: Key]: Val } | Array<Val>;

interface Config<T extends Obj> {
    get<Tret extends T[keyof T]>(key: keyof T, defaultVal: Tret): Tret;
}

type EnabledConfig = {
    enabled?: boolean;
}

class Enabled<T extends EnabledConfig> implements Config<T> {
    public readonly c: T;
    constructor(c?: T) {
        this.c = c || {} as T;
    }
    
    get<Tret extends T[keyof T]>(key: keyof T, defaultVal: Tret): Tret {
        const result: Tret = this.c[key] as Tret;
        return (result === undefined) ? defaultVal : result;
    }
}

type Status = 'yes' | 'no';

type HealthyConfig = EnabledConfig & {
    healthy?: Status;
}

class Healthy {
    public readonly config: Config<HealthyConfig>
    constructor(config: Config<HealthyConfig>) {
        this.config = config;
    }
    get enabled(): boolean {
        return this.config.get<boolean>('enabled', true);
    }
}

const en = new Enabled<HealthyConfig>({enabled: false});
const isEnabled: boolean = en.get<boolean>('enabled', true); // works
const isHealthy: Status = en.get<Status>('healthy', 'no'); // works

const h = new Healthy(new Enabled<HealthyConfig>({enabled: true, healthy: 'no'}));
const enabled: boolean = h.config.get<boolean>('enabled', true); // works
const healthy: Status = h.config.get<Status>('healthy', 'yes'); // works

// This generic "T extends HealthyConfig" causes a problem 
// with the "this.config.get" showing an error on the "<boolean>"
// of "Type 'boolean' does not satisfy the constraint 'T[keyof T]'"
class UnHealthy<T extends HealthyConfig> {
    public readonly config: Config<T>
    constructor(config: Config<T>) {
        this.config = config;
    }
    get enabled(): boolean {
        return this.config.get<boolean>('enabled', true);
    }
}

Solution

  • I've made a little refactor of your code:

    type Key = string | number;
    type Val = Key | boolean | Obj;
    type Obj = { [x: Key]: Val } | Array<Val>;
    
    interface Config<T extends Obj> {
        get<K extends keyof T, V extends T[K]>(key: K, defaultVal: V): V;
    }
    
    type EnabledConfig = {
        enabled?: boolean;
    }
    
    class Enabled<T extends EnabledConfig> implements Config<T> {
        public readonly c: T;
        constructor(c?: T) {
            this.c = c || {} as T;
        }
        
        get<K extends keyof T, V extends T[K]>(key: K, defaultVal: V): V {
            const result: V = this.c[key] as V;
            return (result === undefined) ? defaultVal : result;
        }
    }
    
    type Status = 'yes' | 'no';
    
    type HealthyConfig = EnabledConfig & {
        healthy?: Status;
    }
    
    class Healthy {
        public readonly config: Config<HealthyConfig>
        constructor(config: Config<HealthyConfig>) {
            this.config = config;
        }
        get enabled() {
            return this.config.get('enabled', true);
        }
    }
    
    const en = new Enabled<HealthyConfig>({enabled: false});
    const isEnabled: boolean = en.get('enabled', true); // works
    const isHealthy: Status = en.get('healthy', 'no'); // works
    
    const h = new Healthy(new Enabled<HealthyConfig>({enabled: true, healthy: 'no'}));
    const enabled: boolean = h.config.get('enabled', true); // works
    const healthy: Status = h.config.get('healthy', 'yes'); // works
    
    // This generic "T extends HealthyConfig" causes a problem 
    // with the "this.config.get" showing an error on the "<boolean>"
    // of "Type 'boolean' does not satisfy the constraint 'T[keyof T]'"
    class UnHealthy<T extends Config<HealthyConfig>> {
        public readonly config: T
        constructor(config: T) {
            this.config = config;
        }
        get enabled() {
            return this.config.get('enabled', true);
        }
    }
    

    Is this what you need?