Shanon Jackson
Shanon Jackson

Reputation: 6531

Typescript KeyOf - Array or Object - Desperate

I have the following class.....

export class Person<T, C = T> {
    lens: any = null;
    private value: T;

    constructor(value: T, newLens?: any) {
        this.value = value;
        this.lens = newLens;
    }

    at<K extends keyof C>(path: keyof C): Person<T, C[K]> {
        if(!this.lens) {
            return new Person<T, C[K]>(this.value, Lens.lens(path));
        }
        return new Person<T, C[K]>(this.value, Lens.compose(this.lens)(Lens.lens(path)));
    }

    get(): any {
        if(!this.lens) return this.value;
        return Lens.get(this.lens)(this.value);
    }

    set(f: (newValue: any) => any): T {
        return Lens.set(this.lens)(f(this.get()))(this.value);
    }
}

my problem is that when i try and use my new Person class on an object i get the incorrect behaviour.....

const TestPerson = {
      name: {
          name: "steve"
      },
      siblings: [{name: "shanon"}]
      age: Infinity
}

const test = new Person(TestPerson).at("name").at("name") // works....
const test2 = new Person(TestPerson).at("siblings").at(0) // fails
const test3 = new Person(TestPerson).at(siblings").at("0") // still fails.
const test4 = new Person(TestPerson).at("nonexistantproperty") //correctly fails.

my problem is that i need a "AT" function that can handle keyof objects and keyof arrays, but it seems like no matter how i rework it i can't achieve this.

to me this seems like a massive flaw with typescript, both arrays and objects are just objects under the hood so keyof on an object type and keyof on a array type should work the same way.

Upvotes: 1

Views: 1624

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249676

The simplest solution is to wait until typescript 2.9 (in RC at the time of writing, should be released soon). Prior to 2.9 keyof only returned string indexes, in 2.9 this will include numeric and symbol keys. See the PR. In 2.9 your code will work as expected without any change.

In 2.8 you can use conditional types to achieve a similar effect

type CReturn<C, K extends keyof C> = C extends Array<any> ? C[number] : C[K];
export class Person<T, C = T> {
    lens: any = null;
    private value: T;

    constructor(value: T, newLens?: any) {
        this.value = value;
        this.lens = newLens;
    }

    at<K extends keyof C>(path: C extends Array<any> ? number : K): Person<T, CReturn<C, K>> {
        if(!this.lens) {
            return new Person<T, CReturn<C, K>>(this.value, Lens.lens(path));
        }
        return new Person<T, CReturn<C, K>>(this.value, Lens.compose(this.lens)(Lens.lens(path)));
    }
}
const TestPerson = {
    name: {
        name: "steve"
    },
    siblings: [{ name: "shanon" }],
    age: Infinity
}

const test = new Person(TestPerson).at("name").at("name") // works....
const test2 = new Person(TestPerson).at("siblings").at(0) // ok
const test21 = new Person(TestPerson).at("siblings").at(0).at("name") //ok
const test4 = new Person(TestPerson).at("nonexistantproperty") //correctly fails.

Upvotes: 1

Related Questions