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/sap/capire/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, (books, req) => { })
Actions
In the same manner, actions can be combined with on
:
const { submitOrder } = require('#cds-models/CatalogService')
service.on(submitOrder, ({ data }) => {
// action implementation
})
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:
const { Books } = require('#cds-models/sap/capire/bookshop')
service.on('READ', Books, readBooksHandler)
/** @param { cds.TypedRequest<Books> } req */
function readBooksHandler (req) {
req.data // req.data is now properly known to be of type Books again
}
Using `import` in TypeScript projects:
import { Books } from '#cds-models/sap/capire/bookshop'
service.on('READ', Books, readBooksHandler)
function readBooksHandler (req: cds.TypedRequest<Books>) {
req.data // 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:
namespace incidents;
/** Ticket priority */
type Priority: String enum {
LOW = 'Low';
MEDIUM = 'Medium';
HIGH = 'High';
}
/** Ticket with status and priority */
entity Tickets {
priority: Priority;
/** Assignment status */
status: String enum {
ASSIGNED = 'A';
UNASSIGNED = 'U';
}
}
const { Ticket, Priority } = require('#cds-models/incidents')
service.before('CREATE', Ticket, (req) => {
req.data.priority = Priority.L
req.data.status = Ticket.status.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:
import { Author, Book } from '#cds-models/sap/capire/bookshop'
const myBook = new Book()
// (i) optional chaining
myBook.author?.name
// (ii) explicitly ruling out the undefined and null types
if (myBook.author) myBook.author.name
// (iii) non-null assertion operator
myBook.author!.name
// (iv) explicitly casting your object to a type where all properties are attached
const myAttachedBook = myBook as Required<Book>
myAttachedBook.author?.name
// (v) explicitly casting your object to a type where the required property is attached
const myPartiallyAttachedBook = myBook as Book & { author: Author }
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:
namespace farm;
@singular: 'Mouse'
entity Mice {}
@plural: 'FlockOfSheep'
entity Sheep {}
Generated classes:
import { Mouse, Mice, Sheep, FlockOfSheep } from '#cds-models/farm'
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:
> cds-typer --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 --help This text. --inlineDeclarations --inline_declarations: <flat | structured> (default: structured) Whether to resolve inline type declarations flat: (x_a, x_b, ...) or structured: (x: {a, b}). --IEEE754Compatible --ieee754compatible: <true | false> (default: false) If set to true, floating point properties are generated as IEEE754 compatible '(number | string)' instead of 'number'. --jsConfigPath --js_config_path: <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 --log_level SILENT | ERROR | WARN | INFO | DEBUG | TRACE | SILLY | VERBOSE (default: ERROR) Minimum log level that is printed. The default is only used if no explicit value is passed and there is no configuration passed via cds.env either. --outputDirectory --output_directory: <string> (default: ./) Root directory to write the generated files to. --propertiesOptional --properties_optional: <true | false> (default: true) If set to true, properties in entities are always generated as optional (a?: T). --targetModuleType --target_module_type: <esm | cjs | auto> (default: auto) Output format for generated .js files. Setting it to auto tries to derive the module type from the package.json and falls back to CJS. --useEntitiesProxy --use_entities_proxy: <true | false> (default: false) If set to true the 'cds.entities' exports in the generated 'index.js' files will be wrapped in 'Proxy' objects so static import/require calls can be used everywhere. WARNING: entity properties can still only be accessed after 'cds.entities' has been loaded --version Prints the version of this tool.
Configuration
Any CLI parameter described above can also be passed to cds-typer via cds.env
in the section cds.typer
. For example, so set a project-wide custom output directory for cds-typer to myCustomDirectory
, you would set
cds.typer.output_directory: myCustomDirectory
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 for your local development setup.
- 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
, 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 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 Build Process
Having cds-typer
present as dependency provides the typescript
build task. If your project also depends on the typescript
package, this build task is automatically included when you run cds build
.
If you are customizing your build task, you can add it after the nodejs
build task:
"tasks": [
{ "for": "nodejs" },
{ "for": "typescript" },
…
]
This build task will make some basic assumptions about the layout of your project. For example, it expects all source files to be contained within the root directory. If you find that the standard behavior does not match your project setup, you can customize this build step by providing a tsconfig.cdsbuild.json
in the root directory of your project. We recommend the following basic setup for such a file:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./gen/srv",
},
"exclude": ["app", "gen"]
}
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:
bookshop/
├── 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.
req.data.t
})
})
Similar to cds.entities(…)
, you can't use static imports here. Instead, you need to use dynamic imports. However, there's an exception for static top-level imports.
// ❌ 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')
}}
Static Top-Level Imports since @cap-js/cds-typer 0.26.0
You can pass a new option, useEntitiesProxy
, to cds-typer
. This option allows you to statically import your entities at the top level, as you intuitively would. However, you can still only use these entities in a context where the CDS runtime is fully booted, like in a service definition:
// ✅ top level import now works both during design time and runtime
import { Book } from '#cds-models/sap/capire/bookshop'
// ❌ works during design time, but will cause runtime errors
Book.actions
export class MyService extends cds.ApplicationService {
async init () {
// ✅ cds runtime is fully booted at this point
Book.actions // works
this.on('READ', Book, req => { req.data.author /* works as well */ })
}
}