How I noticed it

I was writing a unit test suite leveraging the test.each table approach of jest.
We have a string literal union type like this:

1
2
3
4
5
export type UnrefRefundReason =
| 'ZERO_DOLLAR_REFUNDABLE'
| 'MULTIPLE_PAYMENTS'
| 'BLIND_REFUND'
| 'DIFFERENT_PAYMENT_TYPES'

And the test table layout looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
test.each([
{
someIds: undefined,
someBoolean: true,
reason: 'DIFFERENT_PAYMENT_TYPES',
},
{
someIds: ['1', '2'],
someBoolean: false,
reason: 'MULTIPLE_PAYMENTS',
},
{
someIds: ['1', '2'],
someBoolean: true,
reason: 'MULTIPLE_PAYMENTS',
},
])(
'goes to the state of unreferencedPrompt with the correct reason $reason if unreferenced refund is needed',
async ({
someIds,
someBoolean,
reason,
}: {
someIds: string[]
someBoolean: boolean
reason: UnrefRefundReason,
}){
//... test script
})

The error I got was:

1
2
Types of property 'reason' are incompatible.
Type 'string' is not assignable to type 'UnrefRefundReason'.

If I go with the other mapping approach (array -> argument list):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test.each([
[undefined, false, 'BLIND_REFUND'],
[[], false, 'BLIND_REFUND'],
[undefined, true, 'DIFFERENT_PAYMENT_TYPES'],
[[], true, 'DIFFERENT_PAYMENT_TYPES'],
[['1', '2'], false, 'MULTIPLE_PAYMENTS'],
[['1', '2'], true, 'MULTIPLE_PAYMENTS'],
])(
'goes to the state of unreferencedPrompt with the correct reason $reason if unreferenced refund is needed',
async (
someIds: string[] | undefined,
someBoolean: boolean,
reason: UnrefRefundReason
) => {
//...test script
}
)

I got similar error:

1
Type '[undefined, boolean, string]' is not assignable to type '[originalRegisterSalePaymentIds: string[] | undefined, originalSalePaidByOtherPT: boolean, reason: UnrefRefundReason]'.

Findings

From string to UnrefRefundReason, TypeScript needs to do a “type narrowing”. But the type narrowing will not propagate into object. See explanation here:

https://github.com/microsoft/TypeScript/issues/31755#issuecomment-498669080

Preference

I am increasingly preferring enum instead of literal union:

1
2
3
4
5
6
export enum UnrefRefundReason {
ZERO_DOLLAR_REFUNDABLE = 'ZERO_DOLLAR_REFUNDABLE',
MULTIPLE_PAYMENTS = 'MULTIPLE_PAYMENTS',
BLIND_REFUND = 'BLIND_REFUND',
DIFFERENT_PAYMENT_TYPES = 'DIFFERENT_PAYMENT_TYPES',
}