1

Typescript challenge (simplified example from more complex code)... Splitting a deep object path and type check the original string.

Is this possible in typescript, or will I have to write runtime checks?

More details in the code comments

interface Residence {
  address: string;
  year: number;
  owner: {
    name: string;
  }
}

const house: Residence = {
  address: 'Type street 1',
  year: 2010,
  owner: {
    name: 'John Smith',
  },
};

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// The following lines work as expected, including giving type errors for non-existing keys:
getProp(house, 'address');
getProp(house, 'year');
// @ts-expect-error: As expected, since 'other' is not part of Residence
getProp(house, 'other');

function deeper<T, P>(obj: T, path: P) {
  // The logic here would be different, the point is splitting up the path
  // to be able to check each part towards the deeper structure of the object
  const firstProp = path.split('.')[0];
  getProp(obj, firstProp);
}

// The problem is when combining flat keys with deep paths. Is the following possible, including type errors for the nonexisting paths?
deeper(house, 'address');
deeper(house, 'owner');
deeper(house, 'owner.name'); // Should work since the deep path exists in the type

// @ts-expect-error: Property 'city' is not in Residence
deeper(house, 'city');
// @ts-expect-error: Deep path owner.email is not in Residence
deeper(house, 'owner.email');
henit
  • 1,150
  • 1
  • 12
  • 28

2 Answers2

4

With TypeScript 4.1 you can indeed almost achieve this with template literal types

type NestedValueOf<Obj, Key extends string> =
  Obj extends object ?
    Key extends `${infer Parent}.${infer Leaf}` ?
      Parent extends keyof Obj ?
        NestedValueOf<Obj[Parent], Leaf>
      : never
    : Key extends keyof Obj ? Obj[Key] : never
  : never

Usage:

interface Residence {
  address: string;
  year: number;
  owner: {
    name: string;
  }
}

const house: Residence = {
  address: 'Type street 1',
  year: 2010,
  owner: {
    name: 'John Smith',
  },
}

function getProp<T, Key extends string>(obj: T, key: Key): NestedValueOf<T, Key> {
  // TODO
}
getProp(house, 'address')    // string
getProp(house, 'owner')      // { name: string }
getProp(house, 'owner.name') // string
getProp(house, 'city')       // never

The key parameter of getProp is not typechecked in this version. This answer might help with that

Joonas
  • 892
  • 6
  • 18
0

Iam afraid you cant do exactly this, you would have to pass in array of string, here is an exmaple how we typed lodash get. (might not be the prettiest but it works and we dont need it anymore with new typescript fetures like new typeguards and optional chaining)

What is the result you are seeking, if you need to check if the object is one from union, you should use typeguards: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

import _ from 'lodash';
export interface TypedExtractor {
    <T, K1 extends keyof T>(object: T, key1: K1): T[K1];
    <T, K1 extends keyof T, K2 extends keyof T[K1]>(object: T, key1: K1, key2: K2): T[K1][K2];
    <T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(object: T, key1: K1, key2: K2, key3: K3): T[K1][K2][K3];
    <T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
    ): T[K1][K2][K3][K4];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
    ): T[K1][K2][K3][K4][K5];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4],
        K6 extends keyof T[K1][K2][K3][K4][K5]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
        key6: K6,
    ): T[K1][K2][K3][K4][K5][K6];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4],
        K6 extends keyof T[K1][K2][K3][K4][K5],
        K7 extends keyof T[K1][K2][K3][K4][K5][K6]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
        key6: K6,
        key7: K7,
    ): T[K1][K2][K3][K4][K5][K6][K7];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4],
        K6 extends keyof T[K1][K2][K3][K4][K5],
        K7 extends keyof T[K1][K2][K3][K4][K5][K6],
        K8 extends keyof T[K1][K2][K3][K4][K5][K6][K7]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
        key6: K6,
        key7: K7,
        key8: K8,
    ): T[K1][K2][K3][K4][K5][K6][K7][K8];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4],
        K6 extends keyof T[K1][K2][K3][K4][K5],
        K7 extends keyof T[K1][K2][K3][K4][K5][K6],
        K8 extends keyof T[K1][K2][K3][K4][K5][K6][K7],
        K9 extends keyof T[K1][K2][K3][K4][K5][K6][K7][K8]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
        key6: K6,
        key7: K7,
        key8: K8,
        key9: K9,
    ): T[K1][K2][K3][K4][K5][K6][K7][K8][K9];
    <
        T,
        K1 extends keyof T,
        K2 extends keyof T[K1],
        K3 extends keyof T[K1][K2],
        K4 extends keyof T[K1][K2][K3],
        K5 extends keyof T[K1][K2][K3][K4],
        K6 extends keyof T[K1][K2][K3][K4][K5],
        K7 extends keyof T[K1][K2][K3][K4][K5][K6],
        K8 extends keyof T[K1][K2][K3][K4][K5][K6][K7],
        K9 extends keyof T[K1][K2][K3][K4][K5][K6][K7][K8],
        K10 extends keyof T[K1][K2][K3][K4][K5][K6][K7][K8][K9]
    >(
        object: T,
        key1: K1,
        key2: K2,
        key3: K3,
        key4: K4,
        key5: K5,
        key6: K6,
        key7: K7,
        key8: K8,
        key9: K9,
        key10: K10,
    ): T[K1][K2][K3][K4][K5][K6][K7][K8][K9][K10];
}

export const _get: TypedExtractor = (object: any, ...keys: (string | number)[]): string | number | object | undefined => {
    return _.get(object, keys);
};
Lukáš Gibo Vaic
  • 3,960
  • 1
  • 18
  • 30