Extract Types from Strings for an Internationalization Library
The first thing to do is capture the results of the
translations that are passed into the first argument of our
We can add
TTranslations as a type argument. Since we care about both the key and the value, we'll say that it
Record<string, string>. Then annotate the translations argument with
0:00 Let's do it. We first need to capture the results of these translations here. We can say TTranslations. We care about both the key and the value. I'm going to put them in as the object shape. Let's say it's a Record string and string, and then TTranslations is going to live inside that, TTranslations.
0:22 Now we've got TTranslations. We can see that the translations are being captured inside this type here, which is great. Next, we need to figure out the key. That key is definitely going to be...Where are we? This is going to be, we're going to say, just keyof TTranslations, for starters.
0:42 Now we can see that everything inside here is going OK. We do have params any inside here, which I'll get to in a minute. Now we should have translations, subtitle. Button, subtitle, title, that's all working. Subtitle is typed as string, which is good.
1:01 Now what we've got. The args, this is the really tricky bit. We need to figure out how to find out which value the user has selected, do a transformation on that value to extract out the parameter keys as union, which we can do with this.
1:20 Then we need to provide some typings to our function in order to make sure that it gives you the parameter or forces you to provide the parameter if it's required and doesn't let you pass it if it's not required. We've got some funky stuff to do.
1:42 Inside here, I can see that I'm going to have to extract out the key that the user used. We can say TKey extends keyof TTranslations, and then TKey is going to live inside here. How did I know that? I know that I need to get hold of one of these. I don't know which one until the user calls the function. I know that it's going to be, in this case, I want to extract out Click me here.
2:11 There's a little trick that I can show you here. When I want to just check that I'm inferring the right thing, then I can say TExample equals TKey, and it's going to be TTranslations TKey. Now I can hover over translate, and I can see that Click me is being inferred there. That's really good.
2:33 If I change this to subtitle, for instance, then I'm going to get "You have count unread messages" being inferred in that third slot. Very handy. We've got this now. Now what we need to do is we can say TExample is GetParamKeysAsUnion.
2:49 Now we should be able to see the translate here. It's never, that's not good. GetParamKeysAsUnion. Am I passing in the right thing there? Right. It's never, because there's nothing in here. There's no key in there whatsoever.
3:10 Whereas if we change this to subtitle, then it's going to be count here. If we said count unread messages with, let's say, second count, then you should be getting count or second count being inferred in that slot. We're getting somewhere.
3:32 Then, now what we need to do is we need to somehow say we want to provide an argument if this doesn't extend never. What I'm going to do is I'm going to keep it in this generic slot. I'm going to say TDynamicKeys is going to be this.
3:54 I'm going to say if TDynamicKeys extends string, if there is at least one there, then we're going to say Record, let's say, TDynamicKeys and string. Otherwise, we're going to return something interesting. Now I'm passing this to an argument thing here. This is a spread property. A rest parameter must be of an array type.
4:20 What I like to do, is in these situations where I need to specify a dynamic number of arguments, is I like to use a tuple. Here, what this is going to do, this is really clever, is it's going to say if DynamicKeys extends string, if it's not never, then what I can do is I'm going to pass in an array with one member. That one member is going to be the arg here.
4:46 This arg, I'm now using a name tuple or, let's say, dynamicArgs. Then what this is going to do is, because we're inside a function here, inside a spread inside a function, it's going to add that to the function as an argument. This is going to add no arguments to it.
5:07 What we end up with, we solved it. This is the solution. What you get is you get translate translations button. If I change this to subtitle, then this is going to yell at me because it's expecting dynamicArgs, which is a record of all the things that subtitle takes. Now I can pass in Record count string. Wow. Fantastic.
5:32 Here, it's going to yell at me if I don't provide it. If I choose title instead, then it's going to say name, Matt, for instance, and then should force you to provide parameters if required and should not let you pass parameters if not required. This is never here. Here we go. That is the solution.
5:50 Now I'm using this TDynamicKeys as a...it's kind of a cheat. What I could do is -- It's not really a cheat, it's a nice little solution -- I can remove it and I could put it back in in those two places. Didn't you notice it was much nicer to have it outside and have it in a default argument?
6:10 TDynamicKeys, even though it's not inferring from anything, using a default parameter here can just save you a little bit of work. It means that this, which is quite an expensive calculation, only needs to be calculated once, instead of the previous example where it was calculated twice here. You can see it was called here and here. Whereas here, it's only calculated once.
6:34 This is a way to save it in a dynamic variable, which is super useful. It also means that it's available when you hover over it, too. You can see that count is available here. There we go. That's the solution. Complicated as hell.
6:46 This params thing here, I don't think there's a way to necessarily not type this as any. I guess I could do Record TDynamicKeys string, but even that, it's pretty tough. I'd have to do some cast here. Even this is extremely difficult. The best thing to do there is just to type it as any because it's just so, so dynamic. It's dynamic based on the value of a string.
7:17 One weakness with this API is that you would need to specify as const on this, because if you don't specify as const, then everything falls apart a little bit. This is always typed as never because it can't extract it out from the string properly. Using as const there is pretty important.
7:34 Well done on if you managed to find the solution or if you managed to just keep up with me as I was explaining it. This is a hardcore one. This this little trick here is the takeaway from this.