Query Properties with keyof and Lookup Types in TypeScript

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet

The keyof operator produces a union type of all known, public property names of a given type. You can use it together with lookup types (aka indexed access types) to statically model dynamic property access in the type system.

Eric
Eric
~ 4 years ago

is there any convenient way to use Keyof on sub-properties? Like

interface Attributed {
  attributes: {
    [k: string]: v: any;
  }
}
interface AttributedTodo extends Attributed {
  id: number;
  attributes: {
    title: string;
  }
}

function getAttribute<T extends Attributed, K keyof T.attributes>(item: T, key: K) {
  return item.attributes[key];
}
getAttribute(todo, "title");

(which doesn't compile)

Marius Schulz
Marius Schulzinstructor
~ 4 years ago

Hi Eric,

try this version:

interface Attributed {
    attributes: {
        [k: string]: any;
    }
}

interface AttributedTodo extends Attributed {
    id: number;
    attributes: {
        title: string;
        completed: boolean;
    }
}

function getAttribute<T extends Attributed, K extends keyof T["attributes"]>(item: T, key: K): T["attributes"][K] {
    return item.attributes[key];
}

const todo: AttributedTodo = {
    id: 1,
    attributes: {
        title: "Mow the lawn",
        completed: false
    }
};

// Type string
const title = getAttribute(todo, "title");

// Type boolean
const completed = getAttribute(todo, "completed");
Wilgert Velinga
Wilgert Velinga
~ 3 years ago

Please note that it is not necessary to implement and use this prop function in order to get the properties of an object. If you use object destructuring the end result is the same, including the type inference!

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const todo: Todo = {
    id: 1,
    text: "Buy milk",
    completed: false
};

const {id, text, completed} = todo;
Filipe Dos Santos Mendes
Filipe Dos Santos Mendes
~ 2 years ago

Hi Marius,

Thank you very much for this course (one which is finally didactic and understandable). I tried to convert your example using a curried function

interface Todo {
  id: number,
  text: string,
  done: boolean
}

const todo: Todo = {
  id: 1,
  text: 'learn TS',
  done: false
}

const prop =
  <T>(obj: T) =>
  <K extends keyof T>(key: K) =>
    obj[key]

const id = prop(todo)('id')

document.body.innerHTML = `Todo ${id}`

It works pretty nice but what if I want to swap the key and the object (like Ramda's prop function)? I tried but K is declared before T and I'm lost there.

Marius Schulz
Marius Schulzinstructor
~ 2 years ago

@Filipe: Glad you liked the course! Take a look at this version and see if it works for you:

interface Todo {
  id: number,
  text: string,
  done: boolean
}

const todo: Todo = {
  id: 1,
  text: 'learn TS',
  done: false
}

const prop =
  <T extends string>(key: T) =>
    <U extends { [P in T]: U[T] }>(value: U) =>
      value[key]

const getID = prop('id')
const id = getID(todo)

document.body.innerHTML = `Todo ${id}`
Dean
Dean
~ 2 years ago

would doing this be "wrong", seems to work. I'm assuming we don't have to because typescript infers this?

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

const id = prop<Todo, 'id'>(todo, "id"); // added the generics here
Marius Schulz
Marius Schulzinstructor
~ 2 years ago

@Dean: No, that's not wrong at all! You're explicitly specifying the type arguments for the prop function call that TypeScript already infers for you. There's no harm in that, but since TypeScript is doing type inference here, I would recommend to leave out the explicit type arguments.