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

A mapped generic type does not work as intended in a generic function #51290

Closed
mckravchyk opened this issue Oct 24, 2022 · 5 comments
Closed
Labels
Fixed A PR has been merged for this issue

Comments

@mckravchyk
Copy link

mckravchyk commented Oct 24, 2022

Bug Report

πŸ”Ž Search Terms

mapped types generic

πŸ•— Version & Regression Information

Not a regression.

This is the behavior in every version I tried, and I reviewed the FAQ.

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type MyPrefixedTypeKeys = 
    'foo' |
    'bar';

type MyPrefixedType<P extends string> = {
    [K in MyPrefixedTypeKeys as `${P}${K}`]: string
}

// Ok
type test = MyPrefixedType<'pre_'>; // { pre_foo: string, pre_bar: string }

// It does not work in a function with a generic parameter
function createPrefixedType<P extends string>(prefix: P): MyPrefixedType<P> {
    let o: MyPrefixedType<P> = {
        [`${prefix}foo`]: '',
        // Note that it does not require the bar property. It does not require any property...
    }

    type x = MyPrefixedType<P>; // ...because the type is seen as { }

    o[`${prefix}bar`] = ''; // Error: Type 'string' is not assignable to type 'MyPrefixedType<P>[`${P}bar`]'.(2322)

    return o;
}

πŸ™ Actual behavior

The mapped type becomes { } in the generic.

πŸ™‚ Expected behavior

The type mapped type should work inside the generic.

Workarounds

In this particular case, it's possible to use Record as a workaround.

type MyPrefixedType2<P extends string> = Record<`${P}${MyPrefixedTypeKeys}`, string>

function createPrefixedType2<P extends string>(prefix: P): MyPrefixedType2<P> {
    let o: MyPrefixedType2<P> = {
        [`${prefix}foo`]: '',
        [`${prefix}bar`]: '',
    } as MyPrefixedType2<P>;

    o[`${prefix}bar`] = '';

    return o;
}
@jcalz
Copy link
Contributor

jcalz commented Oct 24, 2022

Specifically the issue here is key remapping involving generics. Template literal types don't seem to be part of the cause:

type MyPrefixedType<P extends string> = {
  [K in "xxx" as P]: string
}

function createPrefixedType<P extends string>(prefix: P): MyPrefixedType<P> {
  type Z = MyPrefixedType<P>;
  // type Z = {};  
  return {}; // No error!
}

const x = createPrefixedType("XYZ");
console.log(x.XYZ.toUpperCase()); // typed as string!

Playground link

That's obviously a silly thing to do with key remapping, but it illustrates the same issue, I think.

@ahejlsberg
Copy link
Member

This was fixed by #51050. The fix will be in the upcoming 4.9 release.

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Oct 25, 2022
@mckravchyk
Copy link
Author

mckravchyk commented Oct 26, 2022

@ahejlsberg Thanks! The object in the function has the correct type, however, the assignment still creates an error.

type MyPrefixedTypeKeys = 
    'foo' |
    'bar';

type MyPrefixedType<P extends string> = {
    [K in MyPrefixedTypeKeys as `${P}${K}`]: string
}

// Ok
type test = MyPrefixedType<'pre_'>; // { pre_foo: string, pre_bar: string }
let o: test = { pre_bar: '', pre_foo: '' };
o[`pre_bar`] = '';

function createPrefixedType<P extends string>(prefix: P): MyPrefixedType<P> {
    let o: MyPrefixedType<P> = { // Ok
        [`${prefix}foo`]: '',
        [`${prefix}bar`]: '',
    } as MyPrefixedType<P>;

    type x = MyPrefixedType<P>; // The type is correct now

    // but the error still is there
    o[`${prefix}bar`] = ''; // Type 'string' is not assignable to type 'MyPrefixedType<P>[`${P}bar`]'.(2322)

    return o;
}

Playground

@mckravchyk mckravchyk reopened this Oct 26, 2022
@ahejlsberg
Copy link
Member

ahejlsberg commented Oct 26, 2022

Ah, there are actually a couple of issues in play here. The first, illustrated by @jcalz's example, is that we would eagerly resolve MyPrefixedType<P> to just {} when we should instead defer resolution. That was fixed by #51050 and is the reason we now error on the initial assignment to o in the original example (and on the return {} statement in @jcalz's example).

The second issue is the error on the assignment

o[`${prefix}bar`] = '';

The type checker is rather conservative when analyzing assignments to types of the form T[K] where T is a mapped type with an as clause in higher-order form (i.e. an as clause where generics still remain). This is by design. However, it seems to me you don't really need to use an as clause in your example, and things work as expected if you remove it:

type MyPrefixedTypeKeys = 
    'foo' |
    'bar';

type MyPrefixedType<P extends string> = {
    [K in `${P}${MyPrefixedTypeKeys}`]: string
}

function createPrefixedType<P extends string>(prefix: P): MyPrefixedType<P> {
    let o: MyPrefixedType<P> = {
        [`${prefix}foo`]: '',
        [`${prefix}bar`]: '',
    } as MyPrefixedType<P>;

    o[`${prefix}bar`] = '';  // Ok

    return o;
}

@mckravchyk
Copy link
Author

@ahejlsberg Sorry for the late reply. I have found a workaround to express it in a Record type * (since in my particular case, all values in the type where strings) which worked in the older version and I somehow forgot about this thread. It's definitely good to know that it's possible though. Thank you, sir!

type MyPrefixedType<Prefix extends string> = Record<`${Prefix}_${MyPrefixedTypeKeys}`, string>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

3 participants