Skip to content

Commit f723ae5

Browse files
authored
Refactor additional properties handling (#979)
1 parent a41e9bd commit f723ae5

File tree

2 files changed

+111
-64
lines changed

2 files changed

+111
-64
lines changed

output/typescript/types.ts

+44-44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript-generator/index.ts

+67-20
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ function buildInterface (type: M.Interface): string {
197197
const inherits = buildInherits(type, openGenerics)
198198
let code = `export interface ${createName(type.name)}${buildGenerics(type.generics, openGenerics)}${inherits} {\n`
199199
if (type.properties.length === 0 && type.attachedBehaviors == null && inherits.length === 0) {
200-
code += ' [key: string]: never\n'
200+
if (process.env.KIBANA == null) {
201+
code += ' [key: string]: never\n'
202+
}
201203
} else {
202204
for (const property of type.properties) {
203205
code += ` ${cleanPropertyName(property.name)}${required(property)}: ${buildValue(property.type, openGenerics)}\n`
@@ -249,36 +251,81 @@ function buildBehaviorInterface (type: M.Interface): string {
249251
}
250252
}
251253

254+
// Handling additional properties has some caveats. There are 3 ways of defining an interface with additional dyamic properties:
255+
//
256+
// 1.
257+
// interface Foo {
258+
// prop: string
259+
// [key: string]: string
260+
// }
261+
//
262+
// 2.
263+
// interface FooBase {
264+
// prop: string
265+
// }
266+
// type Foo = FooBase & { [key: string]: string }
267+
//
268+
// 3.
269+
// interface FooBase {
270+
// prop: string
271+
// }
272+
// type Foo = FooBase | { [key: string]: number }
273+
//
274+
// 4.
275+
// interface Foo {
276+
// prop: string
277+
// [key: string]: number | string
278+
// }
279+
//
280+
// 1. and 2. are (almost) the same, the important thing is that the dynamic key can also describe every static key,
281+
// in other words, the type of the static keys must be a subset of the type of the dynamic keys.
282+
// If this is not possible, then we should use options 3.
283+
// The best solution for now is 2, where we create an union of all the possible types for the dynamic keys
284+
// (we must use an intersection because option 1 won't work with optional properties).
285+
// The only drawback is that we might allow some type that won't work in the dynamic keys.
286+
// Furthermore, we must take into account the types of the extended class properties (if present).
287+
// The main difference with this approaches comes when you are actually using the types. 1 and 2 are good when
288+
// you are reading an object of that type, while 3 is good when you are writing an object of that type.
252289
function serializeAdditionalPropertiesType (type: M.Interface): string {
253290
const dictionaryOf = lookupBehavior(type, 'AdditionalProperties') ?? lookupBehavior(type, 'AdditionalProperty')
254291
if (dictionaryOf == null) throw new Error(`Unknown implements ${type.name.name}`)
255-
let code = `export interface ${createName(type.name)}Keys${buildGenerics(type.generics)}${buildInherits(type)} {\n`
256-
257-
function required (property: M.Property): string {
258-
return property.required ? '' : '?'
259-
}
260-
292+
const extendedPropertyTypes = getAllExtendedPropertyTypes(type.inherits)
261293
const openGenerics = type.generics?.map(t => t.name) ?? []
294+
let code = `export interface ${createName(type.name)}Keys${buildGenerics(type.generics)}${buildInherits(type)} {\n`
295+
const types: Array<string | number | boolean> = []
262296
for (const property of type.properties) {
263297
code += ` ${cleanPropertyName(property.name)}${required(property)}: ${buildValue(property.type, openGenerics)}\n`
298+
types.push(buildValue(property.type, openGenerics))
264299
}
265300
code += '}\n'
266-
code += `export type ${createName(type.name)}${buildGenerics(type.generics, openGenerics)} = ${createName(type.name)}Keys${buildGenerics(type.generics, openGenerics, true)} |\n`
267-
code += ` { ${buildIndexer(dictionaryOf, openGenerics)} }\n`
301+
302+
// user_defined_value can always "contain" every other type
303+
if (Array.isArray(dictionaryOf.generics) && dictionaryOf.generics[1].kind === 'user_defined_value') {
304+
code += `export type ${createName(type.name)}${buildGenerics(type.generics)} = ${createName(type.name)}Keys${buildGenerics(type.generics, openGenerics, true)}\n & { [property: string]: any }\n`
305+
} else {
306+
const dynamicTypes = Array.isArray(dictionaryOf.generics)
307+
? [...new Set([buildValue(dictionaryOf.generics[1], openGenerics)].concat(types).concat(extendedPropertyTypes))]
308+
: [...new Set(types.concat(extendedPropertyTypes))]
309+
code += `export type ${createName(type.name)}${buildGenerics(type.generics)} = ${createName(type.name)}Keys${buildGenerics(type.generics, openGenerics, true)}\n & { [property: string]: ${dynamicTypes.join(' | ')} }\n`
310+
}
268311
return code
269312

270-
function buildIndexer (type: M.Inherits, openGenerics: string[]): string {
271-
if (!Array.isArray(type.generics)) return ''
272-
assert(type.generics.length === 2)
273-
return `[property: string]: ${buildGeneric(type.generics[1])}`
274-
275-
// generics can either be a value/instance_of or a named generics
276-
function buildGeneric (type: M.ValueOf): string | number | boolean {
277-
const t = typeof type === 'string' ? type : buildValue(type, openGenerics)
278-
// indexers do not allow type aliases so here we translate known
279-
// type aliases back to string
280-
return t === 'AggregateName' ? 'string' : t
313+
function getAllExtendedPropertyTypes (inherit?: M.Inherits): Array<string | number | boolean> {
314+
if (inherit == null) return []
315+
const extendedInterface = model.types.find(type => inherit.type.name === type.name.name && inherit.type.namespace === type.name.namespace)
316+
assert(extendedInterface != null, `Can't find extended type for ${inherit.type.namespace}.${inherit.type.name}`)
317+
assert(extendedInterface.kind === 'interface', `We should be extending from another interface, instead got: ${extendedInterface.kind}`)
318+
const openGenerics = extendedInterface.generics?.map(t => t.name) ?? []
319+
const types: Array<string | number | boolean> = []
320+
for (const property of extendedInterface.properties) {
321+
types.push(buildValue(property.type, openGenerics))
281322
}
323+
types.push(...getAllExtendedPropertyTypes(extendedInterface.inherits))
324+
return [...new Set(types)]
325+
}
326+
327+
function required (property: M.Property): string {
328+
return property.required ? '' : '?'
282329
}
283330
}
284331

0 commit comments

Comments
 (0)