1

I just started using TypeScript in my Node projects and I was wondering if there is a cleaner, more concise way of implementing this:

import { XOR } from "ts-xor";

type _RemoveNull<T> = {
    [P in keyof T] : string;
}

type UserIdParam = {
    a: string;
}

type BudgetIdParam = UserIdParam & {
    b: string | null;
}

type AccountIdParam = _RemoveNull<BudgetIdParam> & {
    c: string | null;
}

type TransIdParam = _RemoveNull<AccountIdParam> & {
    d: string | null;
}

type IdsParam = XOR<XOR<XOR<UserIdParam, BudgetIdParam>, AccountIdParam>, TransIdParam>;

I wanted a type that would accept any of these sample objects:

const a = {a: "1"};
const b = {a: "1", b: "2"};
const c = {a: "1", b: "2", c: "3"};
const d = {a: "1", b: "2", c: "3", d: "4"};

Also, only the last available property of the object can be null, that's why I had to intersect with the previous type and removed the null from the union. I tried to do a union of the four types UserIdParam, BudgetIdParam, AccountIdParam, and TransIdParam but after I read other questions like this, I decided to use an XOR instead (ts-xor) to accomplish what I needed.

Please let me know your thoughts. Thanks!

--

EDIT: as mentioned by @Thomas in the comments, there is no concept of order for the object's properties, so there is no "last" one.

  • Properties in an object don't have an order. It's all up to you to decide what's the "last" property. That's why this is a quite peculiar problem. I'd define a base-type with all properties and then use `Pick | Pick | ...` to select which combinations of properties are allowed. https://tsplay.dev/mL4Gkm – Thomas Mar 04 '22 at 17:53
  • Thanks, I'll check that approach. I might have to do away with the null design because it makes it more complicated. – Erick Kristofer Umali Mar 04 '22 at 18:11

1 Answers1

0

I was able to reimplement it using generics, mapped types, and conditional types (thanks to this article for pointing me in the right direction):

type UserIdKeys = "userId";
type BudgetIdKeys = UserIdKeys | "budgetId" ;
type AccountIdKeys = BudgetIdKeys | "accountId";
type TransactIdKeys = AccountIdKeys | "transactionId";

// added for readability
type AllIdKeys = TransactIdKeys;

type IdParamGroup<T extends AllIdKeys, N extends AllIdKeys> = {
    [P in T]: P extends N ? (string | null) : string;
};

Here's an example usage:

// valid
const trans1: IdParamGroup<TransactIdKeys, "transactionId"> = {
    userId: "1",
    budgetId: "3",
    accountId: "56",
    transactionId: null,
}
// invalid
const budget3: IdParamGroup<BudgetIdKeys, "budgetId"> = {
    userId: "1",
    budgetId: null,
    accountId: "23"
}

You can try it here: TS Playground