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
By extend transitivity, class instances should extend Record<string, any> #57285
Comments
Transitivity of assignment is violated in a number of places in the language, which isnβt guaranteed to be sound. Such violations are unfortunate but intentional consequences of different desirable behaviors interacting poorly. |
I made another test : class Foo {
foo = 4;
}
type T1 = Foo extends Record<string, Foo[keyof Foo]> ? true: false; // false
const obj = {
foo: 4
};
type T2 = typeof obj extends Record<string, Foo[keyof Foo]> ? true: false; // true Shouldn't an instance of Currently, the workaround to this issue is: function faa<T, U extends Record<...> = AsRecord<T> extends Record<...> ? AsRecord<T> : never>(args: U) {
} Can't it be possible to make TS internally/implicitly do it ? |
See #15300. Interfaces (including instances of class declarations) do not get implicit index signatures, while anonymous types (including types inferred for object literals) do. This difference is intentional, and they're not going to change it. |
I'm talking about classes, not interfaces. IIRC classes doesn't have declaration merging. Also, an object works while keeping types on each attributes:
So if it works for objects, why wouldn't it be possible to make it works with a class instance too ? |
The instance side of a class is equivalent to an interface. Your object type example doesn't involve index signatures so it's a different feature entirely. I think I'm going to disengage now; I'm not a member of the TS team, just giving you a heads up as to what's likely going to happen here. |
? but still we have: type T1 = Foo extends Record<string, Foo[keyof Foo]> ? true: false; // false
type T2 = typeof obj extends Record<string, Foo[keyof Foo]> ? true: false; // true |
Uitschrijven wil niet |
The assumption that Why does // @target: ES2022
// adding more keys makes a Record type more specific
type Ex0 = Record<'A', number> extends Record<never, number> ? true : false
// ^?
type Ex1 = Record<'A'|'B', number> extends Record<'A', number> ? true : false
// ^?
type Ex2 = Record<string, number> extends Record<('A'|'B'), number> ? true : false
// ^?
type R = { [K in never]: number }
type RA = { [K in 'A']: number }
type RAB = { [K in ('A'|'B')]: number }
type RS = { [K in string]: number } // could also write this as { [R in 'A'|'B'|string]: number }
// adding more keys makes an Object type more specific
type Ex3 = RA extends R ? true : false
// ^?
type Ex4 = RAB extends RA ? true : false
// ^?
// until we get to [R in string] ? WHAT?
type Ex5 = RS extends RAB ? true : false
// ^? |
I tried to find a cleaner workaround for my issue, and I think I got something related to your question: {
type AsRecord<T> = {
[K in keyof T]: T[K]
}
type ReverseAsRecord<T> = T extends AsRecord<infer C> ? C : never;
type Record_v2<K extends string,V> = ReverseAsRecord<Record<K,V>>;
function foo<T extends Record_v2<string, any>>(t: T): T { throw new Error() }
function faa<T extends Record_v2<string, number>>(t: T): T { throw new Error() }
function fuu<T extends Record_v2<"foo", number>>(t: T): T { throw new Error() }
class C {
foo!: number;
}
foo(new C()); // ok
faa(new C()); // nok, why ??? I'm sad :'(
fuu(new C()); // ok
foo({foo: 3}) // ok
faa({foo: 3}) // ok
fuu({foo: 3}) // ok
} |
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
BTW, I think this is #48070. Also, from experimentation, I'm having a hard time figuring out where it actually makes sense to use a What were you originally trying to accomplish? |
I need a generic object (i.e. with values and a generic type). The only way to do it is to use a generic class (both a value and a type). This need is due to the fact base class expressions cannot references class type expressions. class Events<T> {
CHANGED = EventImpl<[T|null,T|null]>;
}
type AsRecord<C> = {
[K in keyof C]: C[K]
}
class WithEvents<T, I extends EventsCstrs = AsRecord<T> extends EventsCstrs ? AsRecord<T> : never> {
constructor(events: Constructor<I>) {
this._Events = createEventsManager(this, new events() );
}
protected _Events;
get Events(): inferEventsSourcesType<typeof this._Events> {
return this._Events as any;
}
}
export class Test<T> extends WithEvents<Events<T>> {
constructor() {
super(Events);
}
} |
Ah okay. So the motivating issue is, I'm thinking, in the definition of That is, I bet it looks something like: type Constructor<EventTypes extends Record<string, unknown>> And it should look something like this: type Constructor<EventTypes extends object>
// or if you need some fancier restrictions:
type Constructor<
EventTypes extends Record<EventNames, EventValues>,
EventNames extends string = string & keyof EventTypes,
EventValues extends SomeBaseEventType,
> |
No there isn't any issue with WithEvents<T, I extends EventsCstrs = AsRecord<T> extends EventsCstrs ? AsRecord<T> : never> instead of simply writing: WithEvents<I extends Record<string, ...>> But indeed, your solution seems to be working : class Event<T>{}
class X {
foo!: Event<string>;
faa!: Event<number>;
}
function foo<
EventRecord extends Record<EventNames, Event<any>>,
EventNames extends keyof EventRecord = keyof EventRecord,
>( events: Constructor<EventRecord> ) {
return new events();
}
let ev = foo(X);
ev.foo // Event<string> I didn't knew we could make some circular references like that. Could be great to have a kind of "list of known workarounds" in the doc. |
Got it. I'm surprised it even works too :-). You don't even need an intermediate type parameter! An function foo<T extends object & {[K in keyof T]:Event<any>}> (t: T){
// ...
} But there are some restrictions on this technique: // Error: Type parameter 'T' has a circular constraint.
function foo<T extends object & T> (t: T){
} |
Oh, I didn't knew. Indeed it works: function foo<T extends {[K in keyof T]:Event<any>}> (t: Constructor<T>){
return new t();
} function foo<T extends Record<keyof T,Event<any>>> (t: Constructor<T>){
return new t();
} I really think such trick ought to be written somewhere |
π Search Terms
classes instances Record
extend transitivity
π Version & Regression Information
β― Playground Link
Playground Link
π» Code
π Actual behavior
Foo
doesn't extendRecord<string, Foo[keyof Foo]>
, however, it can extend types that extend it.Therefore by transitivity,
Foo
should extendRecord<string, Foo[keyof Foo]>
?π Expected behavior
Foo
should extendRecord<string, Foo[keyof Foo]>
.Additional information about the issue
This issue prevents doing the following:
The text was updated successfully, but these errors were encountered: