2

I have a Column interface that can render values from rows based on the keys:

interface Column<Row, Field extends keyof Row> {
    key: Field;
    render: (value: Row[Field]) => React.ReactNode;
}

If I explicitly set the type of each column, then I get the desired behaviour. For instance, the following is invalid:

interface User {
    id: number;
    name: string;
}

const column: Column<User, "id"> = {
    key: "id",
    render(id: string) {
        return id;
    }
};

since the id property is a string, not a number. Is there a way to get this behaviour without having to specify the type of each column individually? For instance, the following type-checks:

const columns: Array<Column<User, keyof User>> = [
    {
        key: "id",
        render(id: string) {
            return id;
        }
    }
];

since Field is instantiated to keyof User, rather than a specific key.

3
  • How about union type: id: number | string; have a look: stackoverflow.com/questions/38628115/… Commented Sep 15, 2017 at 14:42
  • I'm not sure how that helps: I want TypeScript to only allow id: number as the argument. Commented Sep 15, 2017 at 14:45
  • ohh, OK. didn't understand your question though. Commented Sep 15, 2017 at 14:45

1 Answer 1

4

Ah, I love this type of question. One problem you're running into is that TypeScript won't let you leave out type parameters and infer their types. You want to say something like Column<User> where the Row type parameter is inferred from the object literal you pass to it. Well, we can't do that directly... but we can get this effect indirectly:

function columnsFor<Row>() {
  function of<Field extends keyof Row>(column: Column<Row, Field>): Column<Row, Field> {
    return column;
  }
  return of;
}

The function columnsFor is sort of a curried function; you can explicitly set the Row type parameter, and it returns another function that accepts objects of Column<Row,Field> for any valid type of Field. Here's how you'd use it:

const userColumn = columnsFor<User>();    

const goodColumn = userColumn({
  key: "id",
  render(id: number) {
    return id;
  }
}); // ok, inferred as Column<User, "id">

const badColumn = userColumn({
  key: "id",
  render(id: string) {
    return id;
  }
}); // error as expected 

The userColumn function will only accept an argument of some Column<User, Field> type, and infers Field from that argument. Now you can go ahead and make that array:

const columns = [
  userColumn({
    key: "id",
    render(id: number) {
      return id;
    }
  }), // ok, inferred as Column<User,"id">
  userColumn({
    key: "name",
    render(name: string) {
      return name;
    }
  }), // ok, inferred as Column<User,"name">
  userColumn({
    key: "id",
    render(id: string) {
      return id;
    }
  }) // error as expected
];

Hope that's useful to you. Good luck!

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, I suspected something like this might be possible, but I wasn't sure what the idiomatic approach in TypeScript would be. I was hoping there'd be a way to guarantee correct usage using the type system, since it's still possible to produce incorrect code. You could do that using this approach if TypeScript had opaque types by making Column opaque, so that all columns have to be created using columnFor.
You could make Column a class with a private constructor and give it a static method to produce only the Column objects you want. And the code above isn't exactly incorrect; you told TypeScript you wanted an array of Column<User, "id"|"name">, which isn't really what you meant.
Thanks, I'll take a look at private constructors. As for the original code not being incorrect: I reckon the column isn't a valid instance of Column<User, "id|"name"> since the render method doesn't handle both cases. I think the only reason it type-checks is because of TypeScript's (unsound) bivariant function parameters.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.