Simplified API for custom types - #73
Conversation
insafuhrmann
left a comment
There was a problem hiding this comment.
Thank you @JohannesMeierSE, great work (and a lot of it)! I think this will become a very valuable contribution. While I like the general approach a lot, I have a number of detailed remarks, see comments.
Regarding your open questions:
- I think that users might choose other solutions than the isLanguageNode like specific properties or markers that are unique to LanguageType so it might make sense to make this optional. The default implementation is OK, but it is rather permissive.
- Forbidding string values for TypeSelectors is fine for me
- I think both approaches (SimpleCustomType/ComplexCustomType vs current solution) have their benefits and disadvantages. In any case the user needs to know about it. I think a good documentation is more important than the choice. I tend to support the current solution.
- I'd rather not introduce more of the undefined. I rather have places in which I would like to get rid of it in favour of meaningful type checking (see detail comments). What are the simplifications you have in mind?
| // now use this custom kind to create some custom types | ||
| const matrix2x2 = customKind // "lazy" to use matrix2x2 as 'baseType' => review ZOD, separate primitives and Typir-Types | ||
| .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) | ||
| .finish().getTypeFinal()!; // we know, that the new custom type depends only on types which are already available |
There was a problem hiding this comment.
What needs to be done for custom types otherwise?
There was a problem hiding this comment.
That should be well documented at some point I think. If we keep all in one and do not split into simple and complex custom types we should clarify what holds and is to be kept in mind for each case (no dependencies or dependencies to wait for).
There was a problem hiding this comment.
Yes, we need to document that. If we merge #80 before this PR, I could already write some documentation for custom types.
What needs to be done for custom types otherwise?
If the type is not yet available, you can register a listener, which is called, when the type is available: ....finish().addListener(finishedType => /* now the type is available and can be used */ finishedType.getIdentifier());
There was a problem hiding this comment.
Thanks, that should go into the docu, I think!
There was a problem hiding this comment.
I put this into the documentation
|
|
||
| test('Matrix type', () => { | ||
| const typir = createTypirServicesForTesting(); | ||
| // TODO does not yet work: { factory: { Matrix: services => new CustomKind<MatrixType, TestLanguageNode>(services, { ... }) } } |
|
|
||
| export type CustomTypePropertyInitialization<T extends CustomTypePropertyTypes, LanguageType> = | ||
| // replace Type by a TypeSelector for it ... | ||
| T extends Type ? TypeSelectorForCustomTypes<T, LanguageType> : // note that TypeSelector includes "unknown" (if the LanguageType is not specified), which makes the TypeScript type-checking "useless" here! |
There was a problem hiding this comment.
Does this not negate the benefit of having strong typing for custom type initialisation? Is there any way to avoid unknown here? Maybe with an explicit error type?
There was a problem hiding this comment.
The way to prevent this is not to use unknown as <LanguageType>. I improved the comment here
| } | ||
| } | ||
|
|
||
| // TODO dieses Design für Class and Functions genau so umsetzen/angleichen |
There was a problem hiding this comment.
If this TODO should stay in, make it English?
There was a problem hiding this comment.
This is a reminder for me to change all existing kinds accordingly, if the design for custom types is accepted in the review 🙂 I will do it next week
There was a problem hiding this comment.
see the corresponding commit
| /** Name for this custom kind. */ | ||
| name: string; | ||
|
|
||
| /** This identifier needs to consider all properties which make the custom type unique. The identifiers are used to detect unique custom types. */ |
There was a problem hiding this comment.
From the examples I saw that the identifiers do not consider structure for nested properties. Is uniqueness guaranteed nevertheless?
There was a problem hiding this comment.
It is up to the user of Typir to consider the structure, if necessary. I improved the comment a bit.
JohannesMeierSE
left a comment
There was a problem hiding this comment.
Thank you @insafuhrmann very much for your helpful review!
Regarding the open questions:
I think that users might choose other solutions than the isLanguageNode like specific properties or markers that are unique to LanguageType so it might make sense to make this optional. The default implementation is OK, but it is rather permissive.
Yes. Let's discuss it in the next meeting.
Forbidding string values for TypeSelectors is fine for me
For me as well.
I think both approaches (SimpleCustomType/ComplexCustomType vs current solution) have their benefits and disadvantages. In any case the user needs to know about it. I think a good documentation is more important than the choice. I tend to support the current solution.
I tend to support only the current solution as well, since it reduces maintenance effort. I guess we need to extend custom types in the future and then it is easier to do it only once.
I'd rather not introduce more of the undefined. I rather have places in which I would like to get rid of it in favour of meaningful type checking (see detail comments). What are the simplifications you have in mind?
If you get const value: Type | undefined = ... from some calculation, you cannot use value as return value inside an inference rule. Instead you need to return value === undefined ? InferenceRuleNotApplicable : value instead.
| // now use this custom kind to create some custom types | ||
| const matrix2x2 = customKind // "lazy" to use matrix2x2 as 'baseType' => review ZOD, separate primitives and Typir-Types | ||
| .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) | ||
| .finish().getTypeFinal()!; // we know, that the new custom type depends only on types which are already available |
There was a problem hiding this comment.
Yes, we need to document that. If we merge #80 before this PR, I could already write some documentation for custom types.
What needs to be done for custom types otherwise?
If the type is not yet available, you can register a listener, which is called, when the type is available: ....finish().addListener(finishedType => /* now the type is available and can be used */ finishedType.getIdentifier());
|
|
||
| export type CustomTypePropertyInitialization<T extends CustomTypePropertyTypes, LanguageType> = | ||
| // replace Type by a TypeSelector for it ... | ||
| T extends Type ? TypeSelectorForCustomTypes<T, LanguageType> : // note that TypeSelector includes "unknown" (if the LanguageType is not specified), which makes the TypeScript type-checking "useless" here! |
There was a problem hiding this comment.
The way to prevent this is not to use unknown as <LanguageType>. I improved the comment here
| } | ||
| } | ||
|
|
||
| // TODO dieses Design für Class and Functions genau so umsetzen/angleichen |
There was a problem hiding this comment.
This is a reminder for me to change all existing kinds accordingly, if the design for custom types is accepted in the review 🙂 I will do it next week
| /** Name for this custom kind. */ | ||
| name: string; | ||
|
|
||
| /** This identifier needs to consider all properties which make the custom type unique. The identifiers are used to detect unique custom types. */ |
There was a problem hiding this comment.
It is up to the user of Typir to consider the structure, if necessary. I improved the comment a bit.
…Selectors, calculate identifiers only from custom properties
…, more comments, renamings
…or calculating identifiers of custom types, fixed minor bugs
f5b593b to
e551485
Compare
|
Thanks @insafuhrmann for the helpful joint discussion yesterday! I improved the PR as discussed and wrote some documentation for custom types. |
insafuhrmann
left a comment
There was a problem hiding this comment.
Thank you very much for the improvements, @JohannesMeierSE! Especially the very helpful documentation part and the additional comments! I left a few last detail remarks regarding typos/small mistakes, one of them (a choice of word problem) rather important (might confuse the meaning).
insafuhrmann
left a comment
There was a problem hiding this comment.
Thanks for the last fixes, @JohannesMeierSE. It will be great to have this large contribution merged!
This PR contributes a simplified API to enable users of Typir to define and use their project-specific custom types. Features of custom types as designed in this PR:
custom-cycles.test.tsandcustom-selectors.test.ts)For some more fixes/features of this PR beyond custom types, see the
CHANGELOG.md.Open questions to discuss during the review:
LanguageService.isLanguageNode(node: unknown): node is LanguageType, which is used once and only for a detail (which has some relevance nevertheless): Is there a better solution? Should we make it optional (or is the default implementation OK)? For me, this feels not like a perfect solution.Type), since they cannot be distinguished from string values for primitive properties. In my eyes, that is a reasonable limitation, since you will get a TypeScript error, if you try to do that.getTypeFinal(): Type|undefinedon the initializer always, even in cases, where it is not required, since your custom type has no types as properties or you know, that all depending types are already available. Even in these cases you need the "work-around" withconst myCustomType = ...finish().getTypeFinal()!;. An idea would be to provideSimpleCustomTypes (without support for circular types) andComplexCustomTypes (with support for circular types) and the user of Typir has to select the appropriate solution for the current use case.undefinedis not supported as result to be returned by an inference rule. You need to returnInferenceRuleNotApplicable, if the rule is for another case, or anInferenceProblem. Should we supportundefinedas well? That might simplify some things.Hints for the review:
custom-example-matrix.test.tsandcustom-example-restricted.test.tsfirst to get an idea, how the API works.custom-type.tsandcustom-definitions.ts🙂