|
1 | 1 | import test from 'ava' |
2 | | -import { SettingDefinition, Settings } from '../src/config' |
| 2 | +import { SettingsDefinitionMap, SettingDefinition, Settings } from '../src/config' |
3 | 3 |
|
4 | 4 | // This file has type declarations that test the types of the config system at |
5 | 5 | // compile time. |
@@ -157,6 +157,61 @@ const _settingsType: ExpectEqual< |
157 | 157 | > = true |
158 | 158 | void _settingsType |
159 | 159 |
|
| 160 | +// Reproduces the bug from `packages/sources/liveart/src/config/index.ts`. |
| 161 | +// Annotating the variable with the default `SettingsDefinitionMap` (no |
| 162 | +// inferred generic) widens the settings map to `Record<string, |
| 163 | +// SettingDefinition>`. Any custom setting then resolves via an index |
| 164 | +// signature whose value type is the full `SettingType<SettingDefinition>` |
| 165 | +// union. |
| 166 | +// |
| 167 | +// Previously this collapsed to `undefined` (because `SettingType` was not |
| 168 | +// distributive over `SettingDefinition`), which silently allowed values |
| 169 | +// like `adapterSettings.API_BASE_URL` to be passed into stricter types |
| 170 | +// such as Axios's `baseURL: string | undefined`. |
| 171 | +const wideConfig: SettingsDefinitionMap = { |
| 172 | + API_BASE_URL: { |
| 173 | + type: 'string', |
| 174 | + description: 'API base URL', |
| 175 | + required: true, |
| 176 | + default: 'https://example.com', |
| 177 | + }, |
| 178 | +} |
| 179 | +void wideConfig |
| 180 | + |
| 181 | +const _wideConfigAccess: ExpectEqual< |
| 182 | + Settings<typeof wideConfig>['API_BASE_URL'], |
| 183 | + string | number | boolean | undefined |
| 184 | +> = true |
| 185 | +void _wideConfigAccess |
| 186 | + |
| 187 | +// The tests below are not directly a test of the config types, but it |
| 188 | +// demonstrates an important principle used in SettingType. |
| 189 | + |
| 190 | +type MyType = { foo: number } | { foo: string } |
| 191 | + |
| 192 | +type TypeOfFoo<T extends { foo: unknown }> = T['foo'] extends string |
| 193 | + ? string |
| 194 | + : T['foo'] extends number |
| 195 | + ? number |
| 196 | + : never |
| 197 | + |
| 198 | +type DistributeTypeOfFoo<T extends { foo: unknown }> = T extends never ? never : TypeOfFoo<T> |
| 199 | + |
| 200 | +// If TypeOfFoo is applied directly to MyType, it will evaluate T['foo'] as |
| 201 | +// `string | number`, which does not satisfy either condition and results in |
| 202 | +// `never`. |
| 203 | +const _testTypeOfFooDirectly: ExpectEqual<TypeOfFoo<MyType>, never> = true |
| 204 | +void _testTypeOfFooDirectly |
| 205 | + |
| 206 | +// But if you do the same inside a seemingly useless conditional, it will apply |
| 207 | +// `TypeOfFoo` to each member of the union separately (first to |
| 208 | +// `{ foo: number }` and then to `{ foo: string }`), and then combine the |
| 209 | +// results together. So it will evaluate to `number` for the first member and |
| 210 | +// `string` for the second member, and the result will be `number | string`. |
| 211 | +// then unions the results together. |
| 212 | +const _testTypeOfFooDistributed: ExpectEqual<DistributeTypeOfFoo<MyType>, string | number> = true |
| 213 | +void _testTypeOfFooDistributed |
| 214 | + |
160 | 215 | test('add one actual test so the test framework does not complain', (t) => { |
161 | 216 | t.pass() |
162 | 217 | }) |
0 commit comments