Type Flow
Typr follows dependencies between columns (foreign keys and view dependencies) so that types "flow" from the base column to other tables that reference it. This makes it easy to work with related data using the correct types.
How It Worksβ
When table B has a foreign key to table A's primary key, Typr:
- Uses the same ID type in both places (e.g.,
EmployeeId) - Adds documentation linking to the source column
- Ensures type safety across the relationship
Example: Foreign Key Typesβ
The project_assignment table has foreign keys to both employee and project:
ProjectAssignmentRow
/** Table: showcase.project_assignment Composite primary key: employee_id, project_id */
public record ProjectAssignmentRow(
/** Points to {@link showcase.showcase.employee.EmployeeRow#id()} */
EmployeeId employeeId,
/** Points to {@link showcase.showcase.project.ProjectRow#id()} */
ProjectId projectId,
String role,
/** Default: 0 */
Optional<Integer> hoursAllocated,
Optional<LocalDate> startDate,
Optional<LocalDate> endDate)
implements Tuple6<
EmployeeId,
ProjectId,
String,
Optional<Integer>,
Optional<LocalDate>,
Optional<LocalDate>> {
/** Points to {@link showcase.showcase.employee.EmployeeRow#id()} */
public ProjectAssignmentRow withEmployeeId(EmployeeId employeeId) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
/** Points to {@link showcase.showcase.project.ProjectRow#id()} */
public ProjectAssignmentRow withProjectId(ProjectId projectId) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
public ProjectAssignmentRow withRole(String role) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
/** Default: 0 */
public ProjectAssignmentRow withHoursAllocated(Optional<Integer> hoursAllocated) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
public ProjectAssignmentRow withStartDate(Optional<LocalDate> startDate) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
public ProjectAssignmentRow withEndDate(Optional<LocalDate> endDate) {
return new ProjectAssignmentRow(
employeeId, projectId, role, hoursAllocated, startDate, endDate);
}
public static RowCodec<ProjectAssignmentRow> rowCodec =
RowCodecs.of(
EmployeeId.pgType,
ProjectId.pgType,
PgTypes.text,
PgTypes.int4.opt(),
PgTypes.date.opt(),
PgTypes.date.opt(),
ProjectAssignmentRow::new,
row ->
new Object[] {
row.employeeId(),
row.projectId(),
row.role(),
row.hoursAllocated(),
row.startDate(),
row.endDate()
});
public static PgText<ProjectAssignmentRow> pgText = PgText.from(rowCodec);
public static ProjectAssignmentRow apply(
ProjectAssignmentId compositeId,
String role,
Optional<Integer> hoursAllocated,
Optional<LocalDate> startDate,
Optional<LocalDate> endDate) {
return new ProjectAssignmentRow(
compositeId.employeeId(),
compositeId.projectId(),
role,
hoursAllocated,
startDate,
endDate);
}
@Override
public EmployeeId _1() {
return employeeId;
}
@Override
public ProjectId _2() {
return projectId;
}
@Override
public String _3() {
return role;
}
@Override
public Optional<Integer> _4() {
return hoursAllocated;
}
@Override
public Optional<LocalDate> _5() {
return startDate;
}
@Override
public Optional<LocalDate> _6() {
return endDate;
}
public ProjectAssignmentId compositeId() {
return new ProjectAssignmentId(employeeId, projectId);
}
public ProjectAssignmentId id() {
return this.compositeId();
}
Notice how:
employeeIdhas typeEmployeeId(not justString)projectIdhas typeProjectId(not justString)- Documentation points to the source table
Composite Primary Keysβ
Tables with composite primary keys get a composite ID type that combines the key columns:
ProjectAssignmentId
/** Type for the composite primary key of table `showcase.project_assignment` */
public record ProjectAssignmentId(EmployeeId employeeId, ProjectId projectId)
implements Tuple2<EmployeeId, ProjectId> {
public ProjectAssignmentId withEmployeeId(EmployeeId employeeId) {
return new ProjectAssignmentId(employeeId, projectId);
}
public ProjectAssignmentId withProjectId(ProjectId projectId) {
return new ProjectAssignmentId(employeeId, projectId);
}
public static RowCodec<ProjectAssignmentId> rowCodec =
RowCodecs.of(
EmployeeId.pgType,
ProjectId.pgType,
ProjectAssignmentId::new,
row -> new Object[] {row.employeeId(), row.projectId()});
@Override
public EmployeeId _1() {
return employeeId;
}
@Override
public ProjectId _2() {
return projectId;
}
}
The composite ID preserves the flowed types - EmployeeId and ProjectId remain distinct even within the composite.
Benefitsβ
- Compile-time safety: Can't accidentally pass an
EmployeeIdwhere aProjectIdis expected - IDE navigation: Click-through from foreign key to source table
- Self-documenting: Code shows relationships clearly
- Refactoring confidence: Changing a type reveals all affected code