Overview
The problem appears to be mostly a matter of using the proper generics syntax, which looks like it may reflect a slight misunderstanding while you're learning the new concepts.
I've rewritten your code into a working form below. Pay close attention to where generic parameters appear relative to colons and assignment operators, as it can make the errors quite subtle. The TypeScript compiler doesn't help, because the provided declarations are still valid syntax (except for squiggles on item.title
in nonworking versions), it's just not the correct syntax for what you're trying to achieve. TypeScript doesn't understand what you're trying to do, only what you did :-)
Column<T>
First, let's fix that Column<T>
definition so it properly types the formatter
property. It may be just a typo, but I wanted to mention it. (I also added the missing properties that are used in your MySample.initializeGrid()
method):
export interface Column<T = any> {
field: string;
id: number | string;
name?: string;
sortable?: boolean;
type?: FieldType;
formatter?: Formatter<T>; // originally formatter?: Formatter: T;
}
Formatter<T>
Now, here's your generic Formatter<T>
definition, which is ok:
export declare type Formatter<T = any> = (
row: number, cell: number, value: any, columnDef: Column, item: T
) => string;
However, based on your Column<T>
definition that parametrizes the formatter
property with T
, it looks like you might want to specify the columnDef
arg with the same generic param T
that matches item
, like this:
export declare type Formatter<T = any> = (
row: number, cell: number, value: any, columnDef: Column<T>, item: T
) => string;
But that's an extra detail you'll have to decide. Now onto the main problem...
customEditableInputFormatter
solution (TLDR)
First, before diving into a bunch of TypeScript language syntax details, let me just give the most concise TLDR one-liner working version. It leverages TypeScript's type inference, so all arguments and return value are strongly-typed based on declaring it as Formatter<ReportItem>
:
const customEditableInputFormatter: Formatter<ReportItem> = (r, c, v, d, i) => i.title;
You can write out the full argument names, specify all of their types, specify the return type, and add curly braces around the function body, but this is all that's strictly necessary for strong typing, intellisense, etc.
And here's a slightly longer form that doesn't rely on an IDE to tell the reader what the types are:
const customEditableInputFormatter: Formatter<ReportItem> = (
row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
return item.title; // item is type `ReportItem`
}
If you don't care about ensuring the signature matches a Formatter<T>
to help avoid errors when creating the function, you can even shorten it to this without declaring it as Formatter<ReportItem>
:
const customEditableInputFormatter = (
row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
return item.title; // item is type `ReportItem`
}
TypeScript would still allow the assignment later to Formatter<T>
, but it's prone to error, and I don't recommend it.
customEditableInputFormatter
error analysis
So with that out of the way, let's look at your code in more detail...
This is your original version of the customEditableInputFormatter
declaration:
const customEditableInputFormatter: Formatter = <T = any>(
row: number, cell: number, value: any, columnDef: Column, item: T
) => {
// I want the `item` to be of Type ReportItem but it shows as Type any
// item.title // <-- oops, item is type `T` here, i.e. `any`
};
There are a few things to notice in this version:
- On the left side of the assignment operator (first equals sign), you'll see that you've declared its type as
Formatter
without actually specifying its type argument T
. Therefore, it defaults to any
, as specified in the type declaration (<T = any>
), and it'll never be anything else.
- On the right side of the assigment operator, you have
<T = any>
right before opening parenthesis of the function. This isn't parametrizing the Formatter
type on the left side - that's already been specified in #1. This is declaring a generic parameter for the new anonymous generic function you've created on the right side! Those are two different T
s, which I think is adding confusion here! The declaration is compatable with assignment to a Formatter<any>
, so TypeScript doesn't complain.
- Surprisingly, changing #2 to
<T = ReportItem>
still doesn't fix the problem, even though hovering over item
in the function body says it's type ReportItem
! How can that be? This is a bit tricky, but it's because the function body has to support ALL possible T
s, not just when T
is the default ReportItem
. TypeScript can't know that any random T
will have a title
property, so it won't let you use it on item
even though we know it has one in this case. It's counterintuitive at first, but it'll make sense once you think about it a bit. In any case, the better way to declare it is in the working version above where this isn't an issue.
a suggestion
Finally, I'd recommend removing all of the = any
default type parameters, if you can. I think it played a big part in obscuring what was wrong by preventing TypeScript from helping more directly. You mentioned adding types to an existing codebase, so maybe that makes it more challenging. However, removing them allows the language service to flag issues that default types obscure. They can be quite handy when used judiciously, but I'd skip them unless explicitly needed.
Ok, that's quite a lot, and there are still many other possible things to say about various generics issues like this. However, I'm going to end here since the question is answered. Hopefully, you'll find this helpful and continue your study and use of generics in TypeScript!