Skip to content

Commit 6e36dd1

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

File tree

4 files changed

+83
-14
lines changed

4 files changed

+83
-14
lines changed

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)