Types only for Go, Python and TypeScript
Give restricted people the API of a folder without its implementation: TypeScript declarations, Go and Python stubs that compile, values removed.
Short answer: Types only gives a restricted person the API of a folder without its implementation, so their code still builds against it. It works for TypeScript, Go and Python. Every literal value is replaced and every function body is gone; other files under the folder are hidden.
What each language gets
| Language | What the person's clone contains | What still works |
|---|---|---|
| TypeScript | Generated declarations (.d.ts) in place of .ts and .tsx sources. Literal values are widened: constants, as const objects, tuples, static readonly fields and namespaces carry their types, not their values. |
Imports resolve and the project type-checks. |
| Go | A stub of each source file at the same path, plus the folder's go.mod and a reduced Bazel BUILD.bazel. Exported types, struct fields, interfaces, methods, functions, constants and variables keep their names and types. Every body panics with gitfence: implementation hidden, except while packages initialise, when it returns zero values. |
go build, go vet, go list and gopls on code that imports the package, and tests that don't call into it. |
| Python | Executable Python at the same path. Classes, functions and annotations stay. Every body raises NotImplementedError("gitfence: implementation hidden …"), except while a module is being imported, when it returns None. |
Imports, type checkers, IDE completion, and tests that don't call into it. |
Steps
- Open the repository and its Files tab.
- Choose the folder and set the group's level to Types only.
- Check it with Preview as. The preview shows the generated files a person in that group will receive.
- Save. People in the group get the change on their next
git pull, as a commit called Gitfence: access updated.
What never reaches them
- Values. Constants and variables keep their type but not their value, in every language: a Go
const Timeout = 30 * time.Secondkeeps its type, not the 30. - Unexported code. Private Go code is removed; an unexported type the API needs stays under an opaque name.
- Comments and docstrings. Prose is not reviewed for secrets.
- Tests and test data:
_test.go,testdata/,test_*.py,*_test.pyandconftest.pyare hidden. - Decorator arguments. Python decorators that take arguments (such as
@app.get("/internal")) are dropped: their arguments would disclose values. - Embedded files and build values:
//go:embedtargets and Bazelx_defs,embedsrcs,dataandgo_testrules are hidden. - Files it cannot describe: cgo files, Go files with
//go:build ignore, and anything the parser cannot read. A file that cannot be stubbed is hidden, never served as it is. - Other files under a types-only folder (JSON, SQL,
.envand so on) are hidden. The exceptions are what builds need:go.modandgo.sum, a reducedBUILD.bazelfor Go packages, andpy.typed.
Gotchas
- Code that needs the hidden implementation at run time still cannot run on their machine. Use Fenced CI to run the real tests.
- Types only cannot be edited: a push that changes a generated file is refused.
- Every historical version of a types-only file is stubbed too, so history carries no implementation either.
- The generated header line marks each file:
Generated by Gitfence from fenced source: types only, implementation hidden. Do not edit.
Go monorepos: what works
We test this on a generated monorepo with go.work, nested modules, internal/ and cmd/, protobuf code, Bazel, //go:generate, build tags, generics, init(), go:embed and vendor/.
| Fence | go build / go vet / go list |
go test |
Bazel |
|---|---|---|---|
| A package other code imports is types only | Works | Works, except tests that call the hidden code. Those fail with gitfence: implementation hidden (types only). Tests that need it run in Fenced CI. |
Works: the package gets a BUILD.bazel stub |
| A whole nested module is types only | Works (its go.mod stays) |
Works | Works |
| A nested module nothing visible imports is hidden | Works: its use line in go.work is commented out in the copy and restored on push |
Works | Works |
| A module or package that visible code imports is hidden | Fails for the importing packages | Same | Labels that point at it fail |
.proto sources hidden, generated .pb.go visible |
Works | Works | A go_proto_library that names the hidden proto_library fails |
Generated .pb.go hidden |
Fails | Fails | Fails |
Generated .pb.go types only |
Works | Getters panic like any hidden code | Works |
Advice:
- Fence with types only, not hidden, when other code imports the folder. Hide only what nothing visible imports.
- Commit generated code (
.pb.go,stringeroutput).go generateandbuf generatecan't run without the hidden inputs, so regenerate in the full repository or in Fenced CI. - Tests that need hidden behaviour run in Fenced CI. Locally, a package-level
var x = hidden.New()gets a zero value instead of crashing the whole test binary. A test that relies on that value fails or behaves differently there, so trust Fenced CI's result. - Bazel: keep
proto_librarytargets visible if you build withgo_proto_library. Bazel can't rungo_testfor a types-only package. go work vendor: if a hidden nested module'sgo.modhasreplacelines, Go reports "inconsistent vendoring" in the copy. Make that module types only, or don't vendor the workspace.- Python monorepos work the same way. Namespace packages split across source roots import, and
py.typedand hand-written.pyifiles are kept. A package's__init__.pyre-exports are kept. A call into a types-only module at import time returnsNone. A types-only project's ownpyproject.tomlis hidden, sopip install -edoesn't work for it: add its source folder toPYTHONPATHor your IDE's source roots instead.
Next: access levels and performance and sizing.
Related guides
- Access levelsThe four Gitfence access levels, how rules combine, and what restricted people and AI agents can read, edit or never receive.
- Code reviewThe Gitfence review check, risk areas, approvals and what restricted people see from reviews, checks and comments on GitHub.
- Performance and sizingMeasured on the full Kubernetes history (163,380 commits): one-time setup, clone, fetch and push times, memory, and the host and disk to plan for.