Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type union narrowing upon index access #56943

Closed
6 tasks done
sabberworm opened this issue Jan 3, 2024 · 3 comments
Closed
6 tasks done

Type union narrowing upon index access #56943

sabberworm opened this issue Jan 3, 2024 · 3 comments
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@sabberworm
Copy link

sabberworm commented Jan 3, 2024

🔍 Search Terms

"type union narrowing on multiple properties", "type dependencies", "narrowing indexed", "narrowing argument"

✅ Viability Checklist

⭐ Suggestion

Notice: It’s likely this request has been raised before and I wasn’t using the right search terms. Apologies in advance.

Using type unions and dynamic property access extensively, I have come across the following situation a few times already: There is a discriminating element in the type union that could be used to index a different type that references the corresponding union component. TypeScript could know whether the result from the property access and the union type are structurally related and allow the access. However, as it stands, it needs a little help in form of an explicit conditional (see example below).

📃 Motivating Example

https://www.typescriptlang.org/play?ts=5.3.2#code/JYOwLgpgTgZghgYwgAgPICMBWyDeAoZZOALmQGcwpQBzAbgOXVJAFcBbdaewsiMAQQAUJcpRoBKUgDcA9sAAm3cnwBCgpslYdok5LIX0AvnjABPAA4oACjLJlg6ADYR+CJHZlQyyALwMAPrjmUDLmpADkcOEANHpwjiwQpBRUINSxvGCQUBGZ-OHGhIE4waER6DFxCUma7JxQGXzZuaoF9HgwLCAIYMAyIMpgViHmgjJYpBiYsWCkNnYOzq7uZJ5k4rgMwDCCYAB0pea+Pj7IkeEb+ISE45gA2vuHALq+yPtS8YlKhsgQjryba7IW4PA4jF6nd6fCDfPDGTrdXr9QbLCAeKBjCZoLAzOa2exOFxuNGrLyXLY7R4jY6nc7koEg-aZbJPXZ7D7VcTfX7-FBXa6MvbM6CsqGc2HGPBAA

interface Obj {
  a: string;
  b: number;
  setA(a: string): void;
  setB(b: number): void;
}
type PossibleAccessors =
  | {prop: 'a', value: string, setter: 'setA'}
  | {prop: 'b', value: number, setter: 'setB'};

function setProp(obj: Obj, t: PossibleAccessors) {
  if(t.prop === 'a') {
    obj[t.prop] = t.value;
  } else {
    obj[t.prop] = t.value;
  }
}
function setAccessor(obj: Obj, t: PossibleAccessors) {
  if(t.prop === 'a') {
    obj[t.setter](t.value);
  } else {
    obj[t.setter](t.value);
  }
}

In this example, the ifelse constructs in the functions setProp and setAccessor are entirely superfluous but currently required for successful compilation.

As it stands, with every component added to the PossibleAccessors union, a new control flow branch has to be added to get the code to compile without type casting.

💻 Use Cases

Currently, I resort to type casting in these situations and I enforce conformability between the unions and the corresponding properties using an intricate set of conditional types. This is brittle as there is no nothing informing me when I have set these types up incorrectly. Even when it works, it makes the compile error happen at a location very different from where the run-time error would be.

It would be a tremendous help if TypeScript could narrow the property access correctly without the user having to insert superfluous constructs.

I.e., the following code should just compile™: https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&allowUnreachableCode=true&allowUnusedLabels=true&exactOptionalPropertyTypes=true&noFallthroughCasesInSwitch=true&noImplicitOverride=true&noPropertyAccessFromIndexSignature=true&ts=5.3.2#code/JYOwLgpgTgZghgYwgAgPICMBWyDeAoZZOALmQGcwpQBzAbgOXVJAFcBbdaewsiMAQQAUJcpRoBKUgDcA9sAAm3cnwBCgpslYdok5LIVLqfIbopUQdBkbBrdWzlHoBfPGACeABxQAFGWTLA6AA2EPwISP4yUGTIALwMAD64HlAyHqQA5HAZADR6cEEsEKRmNHm8YJBQmRX8ucjWVZnWdS6ESTgpaZno9VIFRczsDuV8TcgZFSr1jdDNqhlO9HgwLCAIYMAyIMpg3qkegjJYpBiYeWCkvv6BIWERZFFk4rgMx5gA2mAAdF0eALpxZA-fqFCDOFZrDZbHa1cIQSJQI4nNBYC5XPwBYKheGI56vQjvL7fCpVf6CEEDCDiCFAA

@RyanCavanaugh
Copy link
Member

This is already possible, at least for the property side of it:

interface Obj {
  a: string;
  b: number;
  setA(a: string): void;
  setB(b: number): void;
}

type ObjAccessor<K extends keyof Obj> = { prop: K, value: Obj[K] };
;
type PossibleAccessors = ObjAccessor<keyof Obj>;

function setProp<P extends keyof Obj>(obj: Obj, t: ObjAccessor<P>) {
  obj[t.prop] = t.value;
}

declare let obj: Obj;
// OK
setProp(obj, { prop: "a", value: "hello" });
// Error
setProp(obj, { prop: "a", value: 32});

I don't think the "setter" implementation is possible to make check, but you can still represent it in the type system

interface Obj {
  a: string;
  b: number;
  setA(a: string): void;
  setB(b: number): void;
}

type FilterBy<K extends keyof Obj, V> = K extends unknown ? Obj[K] extends (arg: V) => void ? K : never : never;
type ValidSetters<V> = FilterBy<keyof Obj, V>;

type ObjAccessor<K extends keyof Obj> = { prop: K, value: Obj[K], setter: ValidSetters<Obj[K]> };

type PossibleAccessors = ObjAccessor<keyof Obj>;

declare let obj: Obj;
// OK
setProp(obj, { prop: "a", value: "hello", setter: "setA" });
// Error
setProp(obj, { prop: "a", value: "hello", setter: "setB" });

I can't think of a way to analyze your given example that doesn't involve combinatorially exploding the check phase into branches based on every possible constituent of the union, which is definitely not a scalable solution.

@fatcerberus
Copy link

This looks like another vote for #30581

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds labels Jan 4, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Too Complex" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

4 participants