Home > other >  Generics typescript, why does this code throw an error Type '{}' cannot be assigned to a t
Generics typescript, why does this code throw an error Type '{}' cannot be assigned to a t

Time:12-22

I can't figure out why this code is throwing an error. - Type '{}' is not assignable to type 'Keys<T>'.

type Keys<T extends string|symbol>={
    [key in T]: string;
};
const foo = <T extends string|symbol>()=>{
    const a:Keys<T> = {}
    return a
}

Moreover, if you manually substitute the string or symbol type, no errors will be received. Just a warning that T is declared for a function but not used.

Example of working codes:

type Keys<T extends string|symbol>={
    [key in T]: string;
};
const foo = <T extends string|symbol>()=>{
    const a:Keys<string> = {}
    return a
}
type Keys<T extends string|symbol>={
    [key in T]: string;
};
const foo = <T extends string|symbol>()=>{
    const a:Keys<symbol> = {}
    return a
}

You can check the code here

I expected generic code to work fine

CodePudding user response:

Because

const foo = <T = "a" | "b">()=>{
    const a: {a: string, b: string} = {} // <- incompatible
    return a
}

CodePudding user response:

TLDR: Typescript treats wide property types like string much more leniently than property types like 'a' | 'b' | 'c'.


First of all, your Keys type is just the built in Record type which is defined as:

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

So I'm going to use that instead for simplicity.


So why is this the case?

function foo<T extends string | symbol>() {
  const foo: Record<string, string> = {} // fine
  const bar: Record<T, string> = {} // error
}

The answer is because Typescript behaves differently when a key type is infinite, versus when the key type is finite.

  • string could be any string, which string is not tracked.
  • 'a' | 'b' | 'c' is a finite list of strings.

Typescript doesn't bother enforcing the existence of infinite keys because it can't. The type says that any string returns a value, Typescript lets you use it like that.

And this does cause some problems:

const obj: Record<string, string> = { a: 'test' }
obj.b.toUpperCase() // no type error, crash at runtime

A better type for that would be:

const obj: Record<string, string | undefined> = { a: 'test' }
obj.b.toUpperCase() // type error
obj.b?.toUpperCase() // fine

Here the value type might be undefined which means we would have to make sure each property has a value before we treat it as a string. This gives us type safety back.

But when the compiler can know the keys, then it can enforce those keys, and the type checking gets much stricter:

const obj: Record<'a', string> = { a: 'test' }
obj.b.toUpperCase() // type error

Since this now has enough information to apply stronger types, it does.


So what's going here:

const foo = <T extends string|symbol>()=>{
    const a: Record<T, string> = {} // type error
    return a
}

Is that Typescript thinks that T is probably going to be inferred as a finite subset of string | symbol, and not the infinite wider type of string | symbol. So it applies the stricter type checking.

And Typescript is right. Your code does not assign any properties at all, but the types says that this should work:

foo<{ a: number }>().a // number

But your code doesn't ever assign that property so you will get undefined at runtime and that will probably crash something else.

  • Related