Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript confusion in generic `keyof` `extends keyof` `typeof` and others

Tags:

typescript

class Car {
    engine:number;
    detials:{
        good:'Boy'
    }
}

Class ModelProperty<T> when constructed with new ModelProperty<Car>('engine',22); should work as engine is a property of Car and 22 as same type as engine i.e. number.

export class ModelProperty<T, P extends keyof T, V  = T[P]> {
    constructor(public name: P, public value: V) { }
    fun(t: T){
        let value: any = t[this.name]; // Should not be any
        let valueWhyError: V = t[this.name]; //Error.. Why?
    }
}
let engine2 = new ModelProperty<Car,'engine'>('engine','22'); // Gives error as '22' should be number.. working great.
let engine1 = new ModelProperty<Car,'engine'>('engine',2); // But there is repeatation 'engine', 'engine'
  1. In case of engine V should mean number. But that line in function do gives error.
  2. Constructor should not be <Car,'engine'> but only <Car>. Its following properties should be by default depending upon the argument.
  3. How to make it work for nested property like new ModelProperty<Car>(['details','good'],'Girl').
like image 592
amit77309 Avatar asked Apr 11 '18 07:04

amit77309


1 Answers

For the first issue, the problem with your approach for the V parameter is that you specify the default for it, but that does not mean V MUST extend T[P], just that that is the default, you can invoke the constructor with really any type parameter for V. Just use T[P] where appropriate, as even if you constrain it properly (V extends T[P] = T[P]) the compiler it will still not be able to correctly follow that V is assignable from T[P].

export class ModelProperty<T, P extends keyof T> {
    constructor(public name: P, public value: T[P]) { }
    fun(t: T){
        let value = t[this.name]; // is T[P]
    }
}

As for your second issue of repetition, this is an unfortunate side effect of the way type parameters and inference work, if you specify a default for the generic parameter, that default will be used and no inference will take place. If you don't specify the default for K, you can't specify just the value for T, you must also specify K. The simple workaround is to use a two function approach :

export class ModelProperty<T, P extends keyof T> {
    constructor(public name: P, public value: T[P]) { }

    static for<T>() {
        return function <P extends keyof T>(name: P, value: T[P]){
            new ModelProperty<T, P>(name, value);
        } 
    }
}
const carModelCreator = ModelProperty.for<Car>();
let engine2 = carModelCreator('engine','22'); // Gives error as '22' should be number.. working great.
let engine1 = carModelCreator('engine',2); // But there is repeatation 'engine', 'engine'

As for the third issue of nested paths, classes can't have a variable number of type parameters, so you have the option of creating dedicated classes for each path length.

export class ModelProperty2<T, P extends keyof T,  P2 extends keyof T[P]> {
    constructor(public name: [P, P2], public value: T[P][P2]) { }

    static for<T>() {
        return function <P extends keyof T, P2 extends keyof T[P]>(name: [P, P2],  value: T[P][P2]){
            new ModelProperty2<T, P, P2>(name, value);
        } 
    }
}
const carModelCreator = ModelProperty2.for<Car>();
let engine2 = carModelCreator(['detials', 'good'],'22'); //error 
let engine2 = carModelCreator(['detials', 'good'],'Boy'); //ok

Or if you want you can create a single overloaded function that returns an instance ModelProperty where the only type parameter is the value of the last property, and the path is string[]. You get type safety when you create the instance but the info is lost after

export class ModelProperty<T, V> {
    constructor(public name: string[], public value: V) { }


    static for<T>() {
        function helper<P extends keyof T, P2 extends keyof T[P]>(name: [P, P2],  value: T[P][P2])
        function helper<P extends keyof T>(name: [P],  value: T[P])
        function helper(name: string[],  value: any){
            return new ModelProperty<T, any>(name, value);
        } 

        return helper;
    }
}
const carModelCreator = ModelProperty.for<Car>();
let engine1 = carModelCreator(['engine'], 22); // ok
let engine2 = carModelCreator(['detials', 'good'],'Boy'); //ok
like image 171
Titian Cernicova-Dragomir Avatar answered Nov 17 '22 05:11

Titian Cernicova-Dragomir