| label | cds-typer | ||||||
|---|---|---|---|---|---|---|---|
| synopsis | This page explains the package cds-typer in depth. | ||||||
| typedModels |
|
||||||
| status | released |
The following chapter describes the cds-typer package in detail using the bookshop sample as a running example.
- 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:
// @noErrors
const cds = require('@sap/cds')
const service = new cds.ApplicationService
// ---cut---
// without cds-typer
const { Books } = cds.entities('bookshop')
service.before('CREATE', Books, ({ data }) => { /* data is of type any */})
// ^?
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
const service = new cds.ApplicationService
// ---cut---
// ✨ with cds-typer
const { Books } = require('#cds-models/sap/capire/bookshop')
service.before('CREATE', Books, ({ data }) => { /* data is of type Books */})
// ^?::: details 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-more}
Learn about other options to use cds-typer.{.learn-more}
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.
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.
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
// ---cut---
// 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.
The CRUD handlers before, on, and after accept generated types:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
const { Book, Books } = require('#cds-models/sap/capire/bookshop')
const service = new cds.ApplicationService
// ---cut---
// 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) => { })
// ^?In the same manner, actions can be combined with on:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
const service = new cds.ApplicationService
// ---cut---
const { submitOrder } = require('#cds-models/CatalogService')
service.on(submitOrder, ({ data }) => {
// ^?
// action implementation
})::: warning 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:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
const service = new cds.ApplicationService
// ---cut---
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:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
import cds from '@sap/cds'
const service = new cds.ApplicationService
// ---cut---
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
// ^?
}:::
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:
<<< assets/incidents/db/schema.cds
// @paths: {"#cds-models/*": ["%typedModels:incidents:resolved%"]}
const cds = require('@sap/cds')
const service = new cds.ApplicationService
// ---cut---
const { Ticket, Priority } = require('#cds-models/incidents')
service.before('CREATE', Ticket, (req) => {
req.data.priority = Priority.L // [!code focus]
// ^|
req.data.status = Ticket.status.UNASSIGNED // [!code focus]
// ^?
})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; // [!code focus]
…
}
entity Book {
author: Association to Author; // [!code focus]
…
}Generated type file:
class Author {
name?: string // [!code focus]
…
}
class Book {
author?: Association.to<Author> // [!code focus]
…
}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:
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
import cds from '@sap/cds'
// ---cut---
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?.nameNote that (iii) through (v) are specific to TypeScript, while (i) and (ii) can also be used in JavaScript projects.
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:
<<< assets/animal-farm/db/schema.cds{3,6}
Generated classes:
// @paths: {"#cds-models/*": ["%typedModels:farm:resolved%"]}
import { Mouse, Mice, Sheep, FlockOfSheep } from '#cds-models/farm'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.
Besides using the SAP CDS Language Support extension for VS Code, you have the option to use cds-typer on the command line.
npx @cap-js/cds-typer /home/mybookshop/db/schema.cds --outputDirectory /home/mybookshopThe CLI offers several parameters which you can list using the --help parameter.
::: details You should then see the following output:
:::
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
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.
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
rootDirsin 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 distAs 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-modelsMake sure to add the quotes around the asterisk so your shell environment does not expand the pattern.
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:
::: code-group
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./gen/srv",
},
"exclude": ["app", "gen"]
}:::
Type generation can be added to your project as facet via cds add typer.
::: details Under the hood
Adding this facet effectively does four things:
- Adds
@cap-js/cds-typeras 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 :::
::: warning 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.
:::
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.tsEach 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.
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:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
// ---cut---
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.
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
const cds = require('@sap/cds')
// ---cut---
// ❌ 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:
// @noErrors
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
import cds from '@sap/cds'
// ---cut---
// ❌ 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')
}}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:
// @paths: {"#cds-models/*": ["%typedModels:bookshop:resolved%"]}
import cds from '@sap/cds'
// ---cut---
// ✅ 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 */ })
}
}