CDS Typer
The following chapter describes the cds-typer
package in detail using the bookshop sample as a running example.
Quickstart using VS Code
- In your project's root, execute
cds add typer
. - Make sure you have the SAP CDS Language Support extension for VS Code installed.
- See that cds-typer is enabled in your VS Code settings (CDS > Type Generator > Enabled).
- Install the newly added dev-dependency using
npm i
. - Saving any .cds file of your model from VS Code triggers the type generation process.
- Model types now have to be imported to service implementation files by traditional imports of the generated files:
// without cds-typer
const { Books } = cds.entities('bookshop')
service.before('CREATE', Books, ({ data }) => { /* data is of type any */})
// ✨ with cds-typer
const { Books } = require('#cds-models/bookshop')
service.before('CREATE', Books, ({ data }) => { /* data is of type Books */})
How it works:
The extension will automatically trigger the type generator whenever you hit save on a .cds file that is part of your model. That ensures that the generated type information stays in sync with your model. If you stick to the defaults, saving a .cds file will have the type generator emit its type files into the directory @cds-models in your project's root.
Opening your VS Code settings and typing "cds type generator
" into the search bar will reveal several options to configure the type generation process. Output, warnings, and error messages of the process can be found in the output window called "CDS
".
Learn more about the typer
facet.Learn about other options to use cds-typer
.
Using Emitted Types in Your Service
The types emitted by the type generator are tightly integrated with the CDS API. The following section illustrates where the generated types are recognized by CDS.
CQL
Most CQL constructs have an overloaded signature to support passing in generated types. Chained calls will offer code completion related to the type you pass in.
// previous approach (still valid, but prefer using reflected entities over string names)
SELECT('Books') // etc...
// how you can do it using generated types
const { Book, Books } = require('#cds-models/sap/capire/Bookshop')
// SELECT
SELECT(Books)
SELECT.one(Book)
SELECT(Books, b => { b.ID }) // projection
SELECT(Books, b => { b.author(a => a.ID.as('author_id')) }) // nested projection
// INSERT / UPSERT
INSERT.into(Books, […])
INSERT.into(Books).columns(['title', 'ID']) // column names derived from Books' properties
// DELETE
DELETE.from(Books, 42)
Note that your entities will expose additional capabilities in the context of CQL, such as the .as(…)
method to specify an alias.
CRUD Handlers
The CRUD handlers before
, on
, and after
accept generated types:
// the payload is known to contain Books inside the respective handlers
service.before('READ', Books, req => { … }
service.on('READ', Books, req => { … }
service.after('READ', Books, req => { … }
Actions
In the same manner, actions can be combined with on
:
const { submitOrder } = require('#cds-models/sap/capire/Bookshop')
service.on(submitOrder, (…) => { /* implementation of 'submitOrder' */ })
Lambda Functions vs. Fully Fledged Functions
Using anything but lambda functions for either CRUD handler or action implementation will make it impossible for the LSP to infer the parameter types.
You can remedy this by specifying the expected type with one of the following options.
Using JSDoc in JavaScript projects:
service.on('READ', Books, readBooksHandler)
/** @param {{ data: import('#cds-models/sap/capire/Bookshop').Books }} req */
function readBooksHandler (req) {
// req.data is now properly known to be of type Books again
}
Using import type
in TypeScript projects:
import type { Books } from '#cds-models/sap/capire/bookshop'
service.on('READ', Books, readBooksHandler)
function readBooksHandler (req: {{ data: Books }}) {
// req.data is now properly known to be of type Books again
}
Enums
CDS enums are supported by cds-typer
and are represented during runtime as well. So you can assign values to enum-typed properties with more confidence:
type Priority: String enum {
LOW = 'Low';
MEDIUM = 'Medium';
HIGH = 'High';
}
entity Tickets {
priority: Priority;
status: String enum {
ASSIGNED = 'A';
UNASSIGNED = 'U';
}
…
}
const { Ticket, Priority } = require('…')
service.before('CREATE', Ticket, (req) => {
req.data.priority = Priority.LOW
// / \
// inferred as: Priority suggests LOW, MEDIUM, HIGH
req.data.status = Ticket.status.UNASSIGNED
// / \
// inferred as: Tickets_status suggests ASSIGNED, UNASSIGNED
})
Handling Optional Properties
Per default, all properties of emitted types are set to be optional. This reflects how entities can be partial in handlers.
CDS file:
entity Author {
name: String;
…
}
entity Book {
author: Association to Author;
…
}
Generated type file:
class Author {
name?: string
…
}
class Book {
author?: Association.to<Author>
…
}
In consequence, you will get called out by the type system when trying to chain property calls. You can overcome this in a variety of ways:
const myBook: Book = …
// (i) optional chaining
const authorName = myBook.author?.name
// (ii) explicitly ruling out the undefined type
if (myBook.author !== undefined) {
const authorName = myBook.author.name
}
// (iii) non-null assertion operator
const authorName = myBook.author!.name
// (iv) explicitly casting your object to a type where all properties are attached
const myAttachedBook = myBook as Required<Book>
const authorName = myAttachedBook.author.name
// (v) explicitly casting your object to a type where the required property is attached
const myPartiallyAttachedBook = myBook as Book & { author: Author }
const authorName = myPartiallyAttachedBook.author.name
Note that (iii) through (v) are specific to TypeScript, while (i) and (ii) can also be used in JavaScript projects.
Fine Tuning
Singular/ Plural
The generated types offer both a singular and plural form for convenience. The derivation of these names uses a heuristic that assumes entities are named with an English noun in plural form, following the best practice guide.
Naturally, this best practice can't be enforced on every model. Even for names that do follow best practices, the heuristic can fail. If you find that you would like to specify custom identifiers for singular or plural forms, you can do so using the @singular
or @plural
annotations.
CDS file:
// model.cds
@singular: 'Mouse'
entity Mice { … }
@plural: 'FlockOfSheep'
entity Sheep { … }
Generated type file:
// index.ts
export class Mouse { … }
export class Mice { … }
export class Sheep { … }
export class FlockOfSheep { … }
Strict Property Checks in JavaScript Projects
You can enable strict property checking for your JavaScript project by adding the checkJs: true
setting to your jsconfig.json or tsconfig.json. This will consider referencing properties in generated types that are not explicitly defined as error.
Usage Options
Besides using the SAP CDS Language Support extension for VS Code, you have the option to use cds-typer
on the command line.
Command Line Interface (CLI)
npx @cap-js/cds-typer /home/mybookshop/db/schema.cds --outputDirectory /home/mybookshop
The CLI offers several parameters which you can list using the --help
parameter.
You should then see the following output:
> @cap-js/cds-typer@0.22.0 cli
> node lib/cli.js --help
SYNOPSIS
cds-typer [cds file | "*"]
Generates type information based on a CDS model.
Call with at least one positional parameter pointing
to the (root) CDS file you want to compile.
OPTIONS
--IEEE754Compatible: <true | false>
(default: false)
If set to true, floating point properties are generated
as IEEE754 compatible '(number | string)' instead of 'number'.
--help
This text.
--inlineDeclarations: <flat | structured>
(default: structured)
Whether to resolve inline type declarations
flat: (x_a, x_b, ...)
or structured: (x: {a, b}).
--jsConfigPath: <string>
Path to where the jsconfig.json should be written.
If specified, cds-typer will create a jsconfig.json file and
set it up to restrict property usage in types entities to
existing properties only.
--logLevel SILENT | ERROR | WARN | INFO | DEBUG | TRACE | SILLY | VERBOSE
(default: ERROR)
Minimum log level that is printed.
--outputDirectory: <string>
(default: ./)
Root directory to write the generated files to.
--propertiesOptional: <true | false>
(default: true)
If set to true, properties in entities are
always generated as optional (a?: T).
--version
Prints the version of this tool.
Version Control
The generated types are meant to be ephemeral. We therefore recommend that you do not add them to your version control system. Adding the typer as facet will generate an appropriate entry in your project's .gitignore
file. You can safely remove and recreate the types at any time. We especially suggest deleting all generated types when switching between development branches to avoid unexpected behavior from lingering types.
Integrate Into TypeScript Projects
The types emitted by cds-typer
can be used in TypeScript projects as well! Depending on your project setup you may have to do some manual configuration.
- Make sure the directory the types are generated into are part of your project's files. You will either have to add that folder to your
rootDirs
in your tsconfig.json or make sure the types are generated into a directory that is already part of yourrootDir
. - Preferably run the project using
cds-ts
. - If you have to use
tsc
, for example for deployment, you have to touch up on the generated files. Assume your types are in @cds-models below your project's root directory and your code is transpiled to dist/, you would use:
tsc && cp -r @cds-models dist
Integrate Into Your CI
As the generated types are build artifacts, we recommend to exclude them from your software versioning process. Still, as using cds-typer
changes how you include your model in your service implementation, you need to include the emitted files when releasing your project or running tests in your continuous integration pipeline. You should therefore trigger cds-typer
as part of your build process. One easy way to do so is to add a variation of the following command to your build script:
npx @cap-js/cds-typer "*" --outputDirectory @cds-models
Make sure to add the quotes around the asterisk so your shell environment does not expand the pattern.
Integrate Into Your Multitarget Application
Similar to the integration in your CI, you need to add cds-typer
to the build process of your MTA file as well.
build-parameters:
before-all:
- builder: custom
commands:
- npx cds build --production
- npx @cap-js/cds-typer "*" --outputDirectory gen/srv/@cds-models
This integration into a custom build ensures that the types are generated into the gen/srv
folder, so that they are present at runtime.
About The Facet
Type generation can be added to your project as facet via cds add typer
.
Under the hood
Adding this facet effectively does four things:
- Adds
@cap-js/cds-typer
as a dev-dependency (⚠️ which you still have to install usingnpm i
) - Creates (or modifies) a jsconfig.json file to support intellisense for the generated types
- Modifies package.json to enable subpath imports for the generated types
- Adds
@cds-models
(the default output folder for generated files) to your project's .gitignore
TypeScript Projects
Adding the facet in a TypeScript project will adjust your tsconfig.json instead. Note that you may have to manually add the type generator's configured output directory to the rootDirs
entry in your tsconfig.json, as we do not want to interfere with your configuration.
About the Emitted Type Files
The emitted types are bundled into a directory which contains a nested directory structure that mimics the namespaces of your CDS model. For the sake of brevity, we will assume them to be in a directory called @cds-models in your project's root in the following sections. For example, the sample model contains a namespace sap.capire.bookshop
. You will therefore find the following file structure after the type generation has finished:
@cds-models/
└── sap/
└── capire/
└── bookshop/
├── index.js
└── index.ts
Each index.ts file will contain type information for one namespace. For each entity belonging to that namespace, you will find two exports, a singular and a plural form:
// @cds-models/sap/capire/bookshop/index.ts
export class Author { … }
export class Authors { … }
export class Book { … }
export class Books { … }
The singular forms represent the entities from the original model and try to adhere to best practices of object oriented programming for naming classes in singular form. The plural form exists as a convenience to refer to a collection of multiple entities. You can fine tune both singular and plural names that are used here.
You could import these types by using absolute paths, but there is a more convenient way for doing so which will be described in the next section.
Subpath Imports
Adding type support via cds add typer
includes configuring subpath imports. The facet adds a mapping of #cds-models/
to the default path your model's types are assumed to be generated to (<project root>/@cds-models/). If you are generating your types to another path and want to use subpath imports, you will have to adjust this setting in your package.json and jsconfig.json/ tsconfig.json accordingly.
Consider the bookshop sample with the following structure with types already generated into @cds-models:
bookstore/
├── package.json
├── @cds-models/
│ └── ‹described in the previous section›
├── db/
│ ├── schema.cds
│ └── ...
├── srv/
│ ├── cat-service.cds
│ ├── cat-service.js
│ └── ...
└── ...
The following two (equally valid) statements would amount to the same import from within the catalog service:
// srv/cat-service.js
const { Books } = require('../@cds-models/sap/capire/bookshop')
const { Books } = require('#cds-models/sap/capire/bookshop')
These imports will behave like cds.entities('sap.capire.bookshop')
during runtime, but offer you code completion and type hinting at design time:
class CatalogService extends cds.ApplicationService { init(){
const { Book } = require('#cds-models/sap/capire/bookshop')
this.on ('UPDATE', Book, req => {
// in here, req is known to hold a payload of type Book.
// Code completion therefore offers all the properties that are defined in the model.
})
})
Just as with cds.entities(…)
, these imports can't be static, but need to be dynamic:
// ❌ works during design time, but will cause runtime errors
const { Book } = require('#cds-models/sap/capire/bookshop')
class CatalogService extends cds.ApplicationService { init(){
// ✅ works both at design time and at runtime
const { Book } = require('#cds-models/sap/capire/bookshop')
})
In TypeScript you can use type-only imports on top level if you just want the types for annotation purposes. The counterpart for the JavaScript example above that works during design time and runtime is a dynamic import expression:
// ❌ works during design time, but will cause runtime errors
import { Book } from '#cds-models/sap/capire/bookshop'
// ✅ works during design time, but is fully erased during runtime
import type { Book } from '#cds-models/sap/capire/bookshop'
class CatalogService extends cds.ApplicationService { async init(){
// ✅ works both at design time and at runtime
const { Book } = await import('#cds-models/sap/capire/bookshop')
})