Absolutely love generic components. It's so nice to get all the highlighting. I usually extend {id: string}, this way I can add a key for mapping based on the id, and in most cases I have id in all the objects I'm displaying anyway. Another cool use case is a custom dropdown that takes a list of objects. Then you can also have a custom typesafe onChange. Also works great in combination with discriminated union props, if you pass 'multiple' prop your onChange is (value:T[])=>void, if you don't it's value:(T | undefined)=>void. It does get a little messy with so much functionality packed into one component (I also had a dropdown that would make a fetch call for data to populate it, which of course also was returning T[] for type safety), but I'm sure there's a way to make it more concise with some custom hooks and separate components.
THANK YOU! Was having that exact error when spreading a field object for a form today. Will try adding Extends Record to the type first thing tomorrow!
@@mattpocockuk I think the better answer here is that `any` is perfectly good to use in a generic type constraints, since the `any` is never used by an actual instance of a variable, it's just a constraint. If you were typing an actual obj variable whole values could be anything, you should really use Record, so that TS can encourage you to do the appropriate type narrowing or casting on the values that you get from that obj.
@@mattpocockuk I'd think if they both work equally well then unknown is preferable. I avoid any as much as I can, I think of it as just a last resort escape-hatch for when you don't want or can't deal with static typing. It's weird blend of type type and bottom type at the same time.
@@barneylaurance1865OK - but that rule of thumb doesn't make sense in a context where you want the widest type possible. In that case, either are fine.
Wouldn't it be "more react" to render the list of rows outside the Table component and then insert that into the Table children? React's philosophy emphasizes composition over configuration which would make the Table component versatile and more flexible.
I'm not an expert, but I don't think it is crime to have one component with two renderers instead of two components, especially if they never will be used separately, because how you want to use row without table?
It depends on the usecase. Especially in cases of tables, which have fixed structure declared by headers and can be expressed as 2D arrays easily, consuming JS types instead of opaque React components allows to wrap the results in row components with consistent styling. Versatility of children has a price on its parent in the form of forcing it to support a union of all children options. I.e. something like linebreaks for too long values in a column are trivially to compute from a string, which has a known length, while font size and all paddings are known to the table component, since these values are controlled by it in the first place. Interacting with them as components however devolves into hacks around React API and DOM-related boilerplate for counting pixels (good luck dealing with off-by-one errors caused by rounding errors).
Yeah but then the table can't for example do pagination, where the table keeps track of the page, slices the list of data and then only rendere that data with the render function
A very big problem with final code: using lambda fuction as a component will force recriating of everything it renders every time and will make completely impossible to keep any state anywhere inside. One more issue: no keys are specified on rows.
Hi, Matt) Your videos as beautiful as your accent! But I'm here to ask you and your community a question: is there a way in TypeScript I can check on type level whether object has keys or not? I know it is possible with Arrays, but with objects seems to be not
I’m confused a bit because I always use React.FC and pass a props Type as the type arg. I don’t get how in your example you aren’t passing the type, but the generic infers the type by argument order? I’ve seen how you can pass a type argument in the jsx with angle brackets
Wonderful content, as always! I wrote a small article on about this some time ago, inspired by your content about generics. Thank you for your amazing work!
Matt your videos are amazing. I have learned a lot from you I do have a question. Is it possible to use different types for a generic component? for example, I have a carousel I want to create and I want it to be able to display different things. Like it can display an image and become a slider in a sense or it can just be a scrollable like hero section. Is it possible to have something like Carousel and Coraosel where Image and Info are objects that do not have the same attributes. Something like Image has name, urlImg and Info has header, subheader, content
Another subjective opinion is using instead of props.renderRow(row) The former version allows to call hooks inside of it, but it looks strange. The benefit of the former is that you can pass a component renderRow={MyRowComponent} but you lose type inference. I suppose the latter is the right way of rendering the row in this case props.renderRow(row) Btw, great video as usual, short and on point!
Hey Matt, question: See when you hover over react component jsx call, it just references type names instead of actual types like it does when you call is a a function. Is it possible to write a genetic type for ReactNode that solves this issue?
@@QwDragon No, you should not. Any is not restricted and can be considered as a bad practice. In critical environments, it actually is. In those cases, you want to make another component from Table that will correctly wraps the type or pass it as a generic :
How does typescript "decide" which prop to use for the type inference for the type argument T? Take this example: const Table = ({rows, randomProp}: {rows: T[], randomProp: T}) => { return rows } Table({rows: [1,2,3], randomProp: "foobar"}) This will error out saying "Type 'string' is not assignable to type 'number'." Okay, why does rows prop take precedence? Why doesn't this error the other way around, i.e. "Type 'number []' is not assignable to type string [ ]". What makes the "rows" component "win" when it comes to type inference?
Have you tried switching rows and randomProp when calling the function? I think that's where it's inferencing. Another way would be specifying it (e.g. Table(/*args*/) )
Fabulous question. In your example, there is no true way to figure out which one goes first. So TypeScript picks one - I'm sure the compiler knows which, but I don't have an intuition for it. To customize which one gets selected, you can use NoInfer from ts-toolbelt. NoInfer will be coming to TypeScript in 5.4.
do you have any thoughts on the fact we can't type children? ReactNode is more like Record for JSX I'm talking about complex children demanding like 3 childs, , and . Or restrict children to have only one child with data atribute 'foo'...
I have this problem in typescript I encounter from time to time, and no clue how to type it. function assignValue(myObject: TObject, key: TKey, value: Tvalue){...} TObject is usualy easy enough to type Tkey, is just keyof TObject But I have no clue how to get Tvalue to be typeof myObject[key]
The confusing bit for me was that it doesn’t work the way generics usually work, in that you don’t pass a type argument to the component, but instead the generic acts as a placeholder to infer the type of regular props that are passed to the component. And then you can use that inferred type in multiple places in the component.
can't it be a footgun? whenever I see hacks like this I think that such use case was not considered by framework authors and they might not support it. it might stop working at some point or have bugs later along the way.
Speaking from experience this is really bad to show to people. XD I have an app with 10 or more tables and initially someone had idea why not just making single table for all of them... But it doesnt really make sense cause each of these tables is different... has different actions, items, filters...etc... that we ended up with a monster that tides our entire application. Whenever you make change you have to go over entire app to check if everything is working etc. Lesson learned.. just copy the code and have 2 tables or 10 tables that are all simple and clean. Have logic only interesting for them and you dont have to worry to break entire app when making change in single table. So in this situation big no to generics.
I still don't understand the benefit of typescript. This is way more mental load than writing a proper function with guardrails. How is this easier than vanilla js?
Because typescript will now error in the renderrow function if you try to access a row property that doesn't exist. And you get autocomplete if you do 'row.' inside of renderrow. So a much improved DX for the cost of just a few lines of code. The mental load is pretty small if you have some practice with this stuff
@@Netherlands031 maybe i'll figure it out one day, just started with it this year and i've been writing js for over 20 yrs so it's probably a stubborn bias of mine, i'll admit. i'm trying to see it, though
@@deniyii you can type javascript yourself if you write functions for it. that's my whole point. instead of focusing on all this new syntax and stuff it's cleaner code and easier to write when you yourself wrote the typechecking function. const kind of solved this for anyone who puts in a few minutes to write little type checking functions imho.
If you would just stop making stupid const arrow functions and use the dang function keyword it wouldn't get confused about the type argument and you wouldn't have to use the comma.
@@DemanaJaire I mean, I "could leave things being wrong".. But, then, we would still be going to libraries to get information instead of having the internet.. Progress and removing incorrect things must be done
Yes I would like to know why the comma makes a difference as well. Is it just a random buggy thing about typescript or are we missing something fundamental? *Edit* Ok so Matt says @2:40 that adding the comma lets typescript know that is a type parameter, but I didn't understand what else it would be other than a type parameter. Because we're using a const variables, which in a react app can be directly assigned to JSX... like const foo = ... typescript doesn't know that we're not trying to assign JSX. So adding a comma there lets typescript know we're using a type parameter rather than JSX.