diff --git a/language-support/ts/codegen/src/TsCodeGenMain.hs b/language-support/ts/codegen/src/TsCodeGenMain.hs index 80cf29817b71..93fe15ae0170 100644 --- a/language-support/ts/codegen/src/TsCodeGenMain.hs +++ b/language-support/ts/codegen/src/TsCodeGenMain.hs @@ -442,13 +442,17 @@ renderTemplateDef TemplateDef {..} = ((\(tsRef, _, inChcs) -> (tsRef, inChcs)) <$> tplImplements') tplChoices' , [ "export declare const " <> tplName <> ":" - , " damlTypes.Template<" <> tplName <> ", " <> keyTy <> ", '" <> templateId <> "'> & " <> tplName <> "Interface;" + , " damlTypes.Template<" <> tplName <> ", " <> keyTy <> ", '" <> templateId <> "'> &" + , " damlTypes.ToInterface<" <> tplName <> ", " <> implsUnion <> "> &" + , " " <> tplName <> "Interface;" ] ] in (jsSource, tsDecl) where (keyTy, keyDec) = case tplKeyDecoder of Nothing -> ("undefined", DecoderConstant ConstantUndefined) Just d -> (tplName <> ".Key", DecoderLazy d) + implsUnion = if null tplImplements' then "never" + else T.intercalate " | " [ impl | (TsTypeConRef impl, _, _) <- tplImplements' ] templateId = unPackageId tplPkgId <> ":" <> T.intercalate "." (unModuleName tplModule) <> ":" <> @@ -483,8 +487,16 @@ renderInterfaceDef InterfaceDef{ifName, ifChoices, ifModule, ifPkgId} = (jsSourc , [ "};" ] ] tsDecl = T.unlines $ concat - [ifaceDefIface ifName Nothing ifChoices - , ["export declare const " <> ifName <> ": damlTypes.Template ifaceId <> "'> & " <> ifName <> "Interface;"] + [ [ "export declare type " <> ifName <> " = damlTypes.Interface<" + <> renderDecoderConstant (ConstantString ifaceId) <> ">;" ] + , ifaceDefIface ifName Nothing ifChoices + , [ "export declare const " <> ifName <> ":" + , " damlTypes.Template<" <> ifName <> ", undefined, '" <> ifaceId <> "'> &" + -- TODO #14082 pass an intersection of type refs to retroImplements when + -- non-empty; 'unknown' is correct if empty + , " damlTypes.FromTemplate<" <> ifName <> ", unknown> &" + , " " <> ifName <> "Interface;" + ] ] ifaceId = unPackageId ifPkgId <> ":" <> @@ -503,17 +515,16 @@ ifaceDefTempl name mbKeyTy impls choices = , [ "}" ] ] where - mbSubst = Just (Set.fromList . map fst $ impls, implTy) + mbSubst = Nothing keyTy = fromMaybe "undefined" mbKeyTy extension | null impls = "" | otherwise = "extends " <> implTy' - implTy = T.intercalate " & " implRefs implTy' = T.intercalate " , " implRefs implRefs = [if Set.null omit then baseInherit else "Omit<" <> baseInherit <> ", " <> literalOmit <> ">" | ((TsTypeConRef impl, _), omit) <- impls `zip` omitFromExtends - , let baseInherit = impl <> "Interface<" <> name <> ">" + , let baseInherit = impl <> "Interface" literalOmit = T.intercalate " | " . map (renderDecoderConstant . ConstantString) . Set.toList $ omit] @@ -544,16 +555,16 @@ uniques = ifaceDefIface :: T.Text -> Maybe T.Text -> [ChoiceDef] -> [T.Text] ifaceDefIface name mbKeyTy choices = concat - [ ["export declare interface " <> name <> "Interface " <> "{"] + [ ["export declare interface " <> name <> "Interface " <> "{"] , [ " " <> chcName' <> ": damlTypes.Choice<" <> - "T, " <> + name <> ", " <> tsTypeRef (genType chcArgTy mbSubst) <> ", " <> tsTypeRef (genType chcRetTy mbSubst) <> ", " <> keyTy <> ">;" | ChoiceDef{..} <- choices ] , [ "}" ] ] where - mbSubst = Just (Set.singleton (TsTypeConRef name), name <> "Interface") + mbSubst = Nothing keyTy = fromMaybe "undefined" mbKeyTy data ChoiceDef = ChoiceDef diff --git a/language-support/ts/codegen/tests/ts/build-and-lint-test/src/__tests__/test.ts b/language-support/ts/codegen/tests/ts/build-and-lint-test/src/__tests__/test.ts index d9724264c278..ca1429979edd 100644 --- a/language-support/ts/codegen/tests/ts/build-and-lint-test/src/__tests__/test.ts +++ b/language-support/ts/codegen/tests/ts/build-and-lint-test/src/__tests__/test.ts @@ -605,7 +605,7 @@ describe("interface definition", () => { // Something is inherited test("unambiguous inherited is inherited", () => { const c: Choice< - buildAndLint.Main.Asset, + buildAndLint.Lib.Mod.Other, buildAndLint.Lib.Mod.Something, {}, undefined @@ -643,6 +643,8 @@ describe("interface definition", () => { }); test("interfaces", async () => { + const Asset = buildAndLint.Main.Asset; + const Token = buildAndLint.Main.Token; const aliceLedger = new Ledger({ token: ALICE_TOKEN, httpBaseUrl: httpBaseUrl(), @@ -662,8 +664,8 @@ test("interfaces", async () => { ); expect(ifaceContract.payload).toEqual(assetPayload); const [, events1] = await aliceLedger.exercise( - buildAndLint.Main.Asset.Transfer, - ifaceContract.contractId, + Asset.Transfer, + Asset.toInterface(Token, ifaceContract.contractId), { newOwner: BOB_PARTY }, ); expect(events1).toMatchObject([ @@ -687,7 +689,7 @@ test("interfaces", async () => { ); const [, events2] = await bobLedger.exercise( buildAndLint.Main.Token.Transfer, - ifaceContract2.contractId, + Asset.toInterface(Token, ifaceContract2.contractId), { newOwner: ALICE_PARTY }, ); expect(events2).toMatchObject([ diff --git a/language-support/ts/daml-types/index.ts b/language-support/ts/daml-types/index.ts index 4d7b40bc4d37..6c2bbe5ceb0d 100644 --- a/language-support/ts/daml-types/index.ts +++ b/language-support/ts/daml-types/index.ts @@ -51,6 +51,65 @@ export interface Template< Archive: Choice; } +/** + * A mixin for [[Template]] that provides the `toInterface` and + * `unsafeFromInterface` contract ID conversion functions. + * + * Even templates that directly implement no interfaces implement this, because + * this also permits conversion with interfaces that supply retroactive + * implementations to this template. + * + * @typeparam T The template type. + * @typeparam IfU The union of implemented interfaces, or `never` for templates + * that directly implement no interface. + */ +export interface ToInterface { + // overload for direct interface implementations + toInterface( + ic: FromTemplate, + cid: ContractId, + ): ContractId; + // overload for retroactive interface implementations + toInterface(ic: FromTemplate, cid: ContractId): ContractId; + + // overload for direct interface implementations + unsafeFromInterface( + ic: FromTemplate, + cid: ContractId, + ): ContractId; + // overload for retroactive interface implementations + unsafeFromInterface( + ic: FromTemplate, + cid: ContractId, + ): ContractId; +} + +const InterfaceBrand: unique symbol = Symbol(); + +/** + * An interface type, for use with contract IDs. + * + * @typeparam IfId The interface ID as a constant string. + */ +export type Interface = { readonly [InterfaceBrand]: IfId }; + +const FromTemplateBrand: unique symbol = Symbol(); + +/** + * Interface for objects representing Daml interfaces. This supplies the basis + * for the methods of [[ToInterface]]. + * + * Even interfaces that retroactively implement for no templates implement this, + * because forward implementations still require this marker to work. + * + * @typeparam If The interface type. + * @typeparam TX The intersection of template types this interface retroactively + * implements, or `unknown` if there are none. + */ +export interface FromTemplate { + readonly [FromTemplateBrand]: [If, TX]; +} + /** * Interface for objects representing Daml choices. * @@ -87,13 +146,25 @@ export interface Choice { choiceName: string; } +function toInterfaceMixin(): ToInterface { + return { + toInterface(_: FromTemplate, cid: ContractId) { + return cid as ContractId as ContractId; + }, + + unsafeFromInterface(_: FromTemplate, cid: ContractId) { + return cid as ContractId as ContractId; + }, + }; +} + /** * @internal */ -export function assembleTemplate( +export function assembleTemplate( template: Template, - ...interfaces: Template[] -): Template { + ...interfaces: FromTemplate[] +): Template & ToInterface { const combined = {}; const overloaded: string[] = []; for (const iface of interfaces) { @@ -102,7 +173,11 @@ export function assembleTemplate( return undefined; }); } - return Object.assign(_.omit(combined, overloaded), template); + return Object.assign( + _.omit(combined, overloaded), + toInterfaceMixin(), + template, + ); } /**