Skip to content

Commit 35b060d

Browse files
feat(macro): make select macro choices strictly typed
1 parent d77d6c1 commit 35b060d

File tree

8 files changed

+144
-14
lines changed

8 files changed

+144
-14
lines changed

packages/babel-plugin-lingui-macro/src/macroJsAst.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,5 +372,34 @@ describe("js macro", () => {
372372
},
373373
})
374374
})
375+
376+
it("select without other", () => {
377+
const exp = parseExpression(
378+
`select(gender, {
379+
male: "he",
380+
female: "she",
381+
})`
382+
)
383+
const tokens = tokenizeChoiceComponent(
384+
(exp as NodePath<CallExpression>).node,
385+
JsMacroName.select,
386+
createMacroCtx()
387+
)
388+
expect(tokens).toMatchObject({
389+
format: "select",
390+
name: "gender",
391+
options: expect.objectContaining({
392+
female: "she",
393+
male: "he",
394+
offset: undefined,
395+
other: "", // <- other should be filled with empty string
396+
}),
397+
type: "arg",
398+
value: {
399+
name: "gender",
400+
type: "Identifier",
401+
},
402+
})
403+
})
375404
})
376405
})

packages/babel-plugin-lingui-macro/src/macroJsAst.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ export function tokenizeChoiceComponent(
180180
format: format,
181181
options: {
182182
offset: undefined,
183+
/** Default to fill other with empty value for compatibility with ICU spec */
184+
other: "",
183185
},
184186
}
185187

packages/babel-plugin-lingui-macro/src/macroJsx.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,5 +322,33 @@ describe("jsx macro", () => {
322322
},
323323
})
324324
})
325+
326+
it("Select without other", () => {
327+
const macro = createMacro()
328+
const exp = parseExpression(
329+
`<Select
330+
value={gender}
331+
male="he"
332+
one="heone"
333+
female="she"
334+
/>`
335+
)
336+
const tokens = macro.tokenizeNode(exp)
337+
expect(tokens[0]).toMatchObject({
338+
format: "select",
339+
name: "gender",
340+
options: {
341+
female: "she",
342+
male: "he",
343+
offset: undefined,
344+
other: "", // <- other should be filled with empty string
345+
},
346+
type: "arg",
347+
value: {
348+
name: "gender",
349+
type: "Identifier",
350+
},
351+
})
352+
})
325353
})
326354
})

packages/babel-plugin-lingui-macro/src/macroJsx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ export class MacroJSX {
297297
value: undefined,
298298
options: {
299299
offset: undefined,
300+
/** Default to fill other with empty value for compatibility with ICU spec */
301+
other: "",
300302
},
301303
}
302304

packages/core/macro/__typetests__/index.test-d.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,9 @@ expectType<string>(
266266
//// Select
267267
///////////////////
268268

269-
const gender = "male"
269+
type Gender = "male" | "female"
270+
const gender = "male" as Gender // make the type less specific on purpose
271+
270272
expectType<string>(
271273
select(gender, {
272274
// todo: here is inconsistency between jsx macro and js.
@@ -280,6 +282,36 @@ expectType<string>(
280282
})
281283
)
282284

285+
expectType<string>(
286+
// @ts-expect-error: missing required property and other is not supplied
287+
select(gender, {
288+
male: "he",
289+
})
290+
)
291+
292+
expectType<string>(
293+
// missing required property is okay, if other is supplied as fallback
294+
select(gender, {
295+
male: "he",
296+
other: "they",
297+
})
298+
)
299+
300+
expectType<string>(
301+
select(gender, {
302+
// @ts-expect-error extra properties are not allowed
303+
incorrect: "",
304+
})
305+
)
306+
307+
expectType<string>(
308+
select(gender, {
309+
// @ts-expect-error extra properties are not allowed even with other fallback
310+
incorrect: "",
311+
other: "they",
312+
})
313+
)
314+
283315
expectType<string>(
284316
// @ts-expect-error value could be strings only
285317
select(5, {

packages/core/macro/index.d.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,21 @@ export function selectOrdinal(
154154
options: ChoiceOptions
155155
): string
156156

157-
type SelectOptions = {
157+
type SelectOptionsExhaustive<T extends string = string> = {
158+
[key in T]: string
159+
}
160+
161+
type SelectOptionsNonExhaustive<T extends string = string> = {
158162
/** Catch-all option */
159163
other: string
160-
[matches: string]: string
164+
} & {
165+
[key in T]?: string
161166
}
162167

168+
type SelectOptions<T extends string = string> =
169+
| SelectOptionsExhaustive<T>
170+
| SelectOptionsNonExhaustive<T>
171+
163172
/**
164173
* Selects a translation based on a value
165174
*
@@ -180,9 +189,9 @@ type SelectOptions = {
180189
* @param value The key of choices to use
181190
* @param choices
182191
*/
183-
export function select(
184-
value: string | LabeledExpression<string>,
185-
choices: SelectOptions
192+
export function select<T extends string = string>(
193+
value: T | LabeledExpression<T>,
194+
choices: SelectOptions<T>
186195
): string
187196

188197
/**

packages/react/macro/__typetests__/index.test-d.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
import React from "react"
1212
import { ph } from "@lingui/core/macro"
1313

14-
const gender = "male"
14+
type Gender = "male" | "female"
15+
const gender = "male" as Gender
1516
const user = {
1617
name: "John",
1718
}
@@ -126,8 +127,20 @@ m = (
126127
// @ts-expect-error: `value` could be string only
127128
m = <Select value={5} other={"string"} />
128129

129-
// @ts-expect-error: `other` required
130-
m = <Select value={"male"} />
130+
// @ts-expect-error: `other` required unless exhaustive
131+
m = <Select value={gender} />
132+
133+
// @ts-expect-error: `other` required unless exhaustive
134+
m = <Select value={gender} _male="..." />
135+
136+
// @ts-expect-error: `other` required unless exhaustive
137+
m = <Select value={gender} _female="..." />
138+
139+
// non-exhaustive okay if other is defined
140+
m = <Select value={gender} _female="..." other="..." />
141+
142+
// exhaustive okay without other
143+
m = <Select value={gender} _male="..." _female="..." />
131144

132145
// @ts-expect-error: `value` required
133146
m = <Select other={"male"} />

packages/react/macro/index.d.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,25 @@ type PluralChoiceProps = {
3232
[digit: `_${number}`]: ReactNode
3333
} & CommonProps
3434

35-
type SelectChoiceProps = {
36-
value: string | LabeledExpression<string | number>
35+
type SelectChoiceOptionsExhaustive<T extends string = string> = {
36+
[key in T as `_${key}`]: ReactNode
37+
}
38+
39+
type SelectChoiceOptionsNonExhaustive<T extends string = string> = {
3740
/** Catch-all option */
3841
other: ReactNode
39-
[option: `_${string}`]: ReactNode
40-
} & CommonProps
42+
} & {
43+
[key in T as `_${key}`]?: ReactNode
44+
}
45+
46+
type SelectChoiceOptions<T extends string = string> =
47+
| SelectChoiceOptionsExhaustive<T>
48+
| SelectChoiceOptionsNonExhaustive<T>
49+
50+
type SelectChoiceProps<T extends string = string> = {
51+
value: T | LabeledExpression<T>
52+
} & SelectChoiceOptions<T> &
53+
CommonProps
4154

4255
/**
4356
* Trans is the basic macro for static messages,
@@ -105,7 +118,9 @@ export const SelectOrdinal: VFC<PluralChoiceProps>
105118
* />
106119
* ```
107120
*/
108-
export const Select: VFC<SelectChoiceProps>
121+
export const Select: {
122+
<T extends string = string>(props: SelectChoiceProps<T>): React.JSX.Element
123+
}
109124

110125
declare function _t(descriptor: MacroMessageDescriptor): string
111126
declare function _t(

0 commit comments

Comments
 (0)