I'm trying to generate a generic function that allow me to generate a strongly typed setter for a given type, with a callback - e.g:
interface Foo {
a: number,
b: string
}
magick('a', 43) => {} // Should work
magick('a', '43') => {} // Should fail
I've implemented a generic function that does this - and it works. But however if I try to copy that function type safety is not enforced (or more likely I'm misunderstanding typescript!)
interface Test {
a: number;
b: string;
}
interface Test2 {
a2: boolean;
b2: '+' | '-';
}
const testVal: Test = {
a: 42,
b: 'test',
};
type Demo<T> = <K extends keyof T> (key: K, val: T[K]) => void
const implDemo: Demo<Test> = (key, val) => {
testVal[key] = val;
};
Firstly - the function is working as how I want it:
/* prints: {a: 10, b: "test"} - correct */
implDemo('a', 10); console.log(testVal);
/* Fails as expected - type safety - a should be number */
implDemo('a', 'text');
But why is this following possible? How Can Demo<Test2>
be assignable to Demo<Test>
/* Create a pointer to implDemo - but with WRONG type!!! */
const implDemo2: Demo<Test2> = implDemo;
implDemo2('a2', true);
console.log(testVal);
/* prints: {a: 10, b: "test", a2: true} - we just violated out type!!!! */
What we just did above is the same as doing:
testVal['a2'] = true; /* Doesn't work - which it shouldn't! */
Here is another simply type, where type safety is actually enforced
type Demo2<T> = (val: T) => void;
const another: Demo2<string> = (val) => {};
/* This fails - as expected */
const another2: Demo2<number> = another;
Is this a bug in typescript - or am I misunderstanding something? My suspicion is that type Demo<T> = <K extends keyof T>
is the culprit, but I simply don't understand how I'm allowed to "hack" the typesystem this way.
Bookmark this question. Show activity on this post. I had an impression that TypeScript allowed you to take a valid JavaScript program and "enforce" types by making a few key symbols type-safe.
It seemed like TypeScript was failing to live up to its potential regarding type safety. Fortunately, with some non-default configuration and some discipline, it is possible to get a substantial degree of both compile-time and run-time type safety from TypeScript.
Assigning Generic ParametersBy passing in the type with the <number> code, you are explicitly letting TypeScript know that you want the generic type parameter T of the identity function to be of type number . This will enforce the number type as the argument and the return value.
Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.
I'd say this is a compiler bug. I'm not sure if it has been reported yet; so far I haven't found anything by searching, but that doesn't mean it isn't there. EDIT: I've filed an issue about this behavior; we'll see what happens. UPDATE: looks like this might be fixed for TS3.5 TS3.6 TS3.7... who knows ๐
!
Here's a reproduction:
interface A { a: number; }
interface B { b: string; }
type Demo<T> = <K extends keyof T> (key: K, val: T[K]) => void
// Demo<A> should not be assignable to Demo<B>, but it is?!
declare const implDemoA: Demo<A>;
const implDemoB: Demo<B> = implDemoA; // no error!? ๐
// Note that we can manually make a concrete type DemoA and DemoB:
type DemoA = <K extends keyof A>(key: K, val: A[K]) => void;
type DemoB = <K extends keyof B>(key: K, val: B[K]) => void;
type MutuallyAssignable<T extends U, U extends V, V=T> = true;
// the compiler agrees that DemoA and Demo<A> are mutually assignable
declare const testAWitness: MutuallyAssignable<Demo<A>, DemoA>; // okay
// the compiler agrees that DemoB and Demo<B> are mutually assignable
declare const testBWitness: MutuallyAssignable<Demo<B>, DemoB>; // okay
// And the compiler *correctly* sees that DemoA is not assignable to DemoB
declare const implDemoAConcrete: DemoA;
const implDemoBConcrete: DemoB = implDemoAConcrete; // error as expected
// ~~~~~~~~~~~~~~~~~ <-- Type 'DemoA' is not assignable to type 'DemoB'.
๐Link to code๐จโ๐ป
You can see that DemoA
and Demo<A>
are basically the same type (they are mutually assignable, meaning a value of one type can be assigned to a variable of the other). And DemoB
and Demo<B>
are also the same type. And the compiler does understand that DemoA
is not assignable to DemoB
.
But the compiler thinks that Demo<A>
is assignable to Demo<B>
, which is your problem. It's as if the compiler "forgets" what T
in Demo<T>
is.
If you really need to work around this for now you might want to brand Demo<T>
with something that will "remember" what T
is, but doesn't stop you from assigning your implementation to it:
// workaround
type Demo<T> = (<K extends keyof T> (key: K, val: T[K]) => void) & {__brand?: T};
const implDemoA: Demo<A> = (key, val) => {} // no error
const implDemoB: Demo<B> = implDemoA; // error here as expected
Okay, hope that helps; good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With