Skip to content

Bug: Argument of string literals list can't be used to construct template literal type #43198

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

Closed
trxcllnt opened this issue Mar 11, 2021 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@trxcllnt
Copy link

Bug Report

An argument of string literals list can't be used to construct a new template literal type.

This could be a mistake on my part, but I've scoured the issues list and typed template literal PRs/docs and haven't seen anything that should preclude this. Small repro below.

🔎 Search Terms

keyof, typed template literals

🕗 Version & Regression Information

4.1+

⏯ Playground Link

💻 Code

// Borrowed from Template literal types PR:
// https://github.com/microsoft/TypeScript/pull/40336
type Join<T extends unknown[], D extends string> =
    T extends [] ? '' :
    T extends [string | number | boolean | bigint] ? T[0] :
    T extends [string | number | boolean | bigint, ...infer U] ? `${T[0]}${D}${Join<U, D>}` :
    string;

class Foo<T> {
  // Capture type T so we can narrow to `K extends keyof T`
  constructor(private obj: T) {}
  
  // `K extends string[]` doesn't work either
  combineKeys<K extends (keyof T & string)[]>(keys: K) {
    return keys.join('_') as Join<K, '_'>;
  }
}

// key should be type `'a_b'`, but instead is `string`
const key = new Foo({ "a": 0, "b": 1 }).combineKeys(["a", "b"]);

🙁 Actual behavior

key is type string.

🙂 Expected behavior

key should be type 'a_b'

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 12, 2021
@RyanCavanaugh
Copy link
Member

Without an as const, TS won't treat ["a", "b"] as a tuple in this context -- it's ("a" | "b")[] instead. You need an as const with corresponding upstream changes in the types:

type Join<T extends readonly unknown[], D extends string> =
    T extends readonly [] ? '' :
    T extends readonly [string | number | boolean | bigint] ? T[0] :
    T extends readonly [string | number | boolean | bigint, ...infer U] ? `${T[0]}${D}${Join<U, D>}` :
    string;

class Foo<T> {
  // Capture type T so we can narrow to `K extends keyof T`
  constructor(private obj: T) {}
  
  // `K extends string[]` doesn't work either
  combineKeys<K extends readonly  (keyof T & string)[]>(keys: K) {
    return keys.join('_') as Join<K, '_'>;
  }
}

// works
const key = new Foo({ "a": 0, "b": 1 }).combineKeys(["a", "b"] as const);

@trxcllnt
Copy link
Author

trxcllnt commented Mar 12, 2021

@RyanCavanaugh Ah great, the one new feature I haven't gotten up to speed on 😅.

Is this something that can only be at the call-site, or can we capture the const-ness of the argument in the argument's generic type (or something else like internally cast as <readonly K>)?

The example in this issue is intentionally trivial for replicating the issue, but requiring library consumers to pass as const to get proper type inference in a common call like df.groupBy({ by: ["a", "b", "c"] }) is less than ideal.

@epferrari
Copy link

epferrari commented Mar 14, 2021

Very similar to an issue I just ran into. Was very much looking forward to typing tuple members after a rest type, but in practice it's not as elegant as I'd hoped.

interface MessageMap {
  [MessageName: string]: object; // payload
}

interface Message<T extends MessageMap, K extends keyof T> {
  topic: [...string[], K];
  payload: T[K];
}

interface Publisher<T extends MessageMap> {
  publish<K extends keyof T>(message: Message<T, K>): void;
}

interface Subscriber<T extends MessageMap> {
  subscribe<K extends keyof T>(topic: [...string[], K], handler: (msg: Message<T, K>) => void): void;
}

type MyMessageMap = {
    foo: {
        f: string;
    };
    bar: {
        b: boolean;
    };
};

({} as unknown as Subscriber<MyMessageMap>).subscribe(
    ['a', 'b', 'foo'],
    // Argument of type '[string, string, string]' is not assignable to parameter of type '[...string[], "foo"]'.
    // Type at position 2 in source is not compatible with type at position 1 in target.
    // Type 'string' is not assignable to type '"foo"'
    (msg) => console.log(msg.payload.f)
);


({} as unknown as Subscriber<MyMessageMap>).subscribe(
    ['a', 'b', 'foo' as const], // works
    (msg) => console.log(msg.payload.f)
);

({} as unknown as Subscriber<MyMessageMap>).subscribe(
    ['a', 'b', 'foo'],
    (msg) => console.log(msg.payload.b)
    // even without the `const` declaration, it knows this is wrong:
    // Property 'b' does not exist on type '{ f: string; }'
);

https://www.typescriptlang.org/play?ssl=46&ssc=3&pln=1&pc=1#code/JYOwLgpgTgZghgYwgAgLIQM4bgcwquAB2QG8AoZZAbXS1wgDk4BbCALmQzClBwF0OAewBGAKwgIwAbmQB6WckJwAngBtBcACZkAvmTKhIsRClrY8AHgAqyCAA9IITRjSZz+IgBpkAaVsOIJxcAawhlQRhkKwA+UgpkMEFCYAQOKgA6TK4eEBwqPm8fPil4pTUNTQ4rKiKSvQNwaHgkZAAFAFdhVWAMAAtoa39HZ1c6PAJCWPJKQk7uvos-e2GQsIio6IAKVjH2UfdrQuiASg4AN0FgTTr9QyaTZABlTowEHmEBm2XAkbN6Cam8QwLzewA+iyGP1W4UiMU2iWSqWomXS2V4+UKBWQvTgTlU0A42wwOA4f0sViOx2QAF5YhcrqdkPTrrp9GBlIRTMoyR5iNS4pRKDBBIIONNBRKYBw0bkShKdHLBcI4FAxfEJZRhBxhCL8bjFZQFboSmRNiQdMg4C52iBgiBBAB3ECWlzPYSvd4DVDctz-IgnVEgz2bdXUADkcDD3jDwijyDDwsEYYKofkyAAglAcO1WOBkOt2Zz41QZThvKXy9xeHww8gesh7WAXRhgDgQHAuihEooVSwIEZ85FCygwxkslXchjkAAiRPTmvpVMKKwclBwJuEQQtsDAQTOgBMdedGEE7SgLXrjeQCEEzCUO87yAdwDAvQSq8tG63L93zoAjEeCQqngYCLhKaYrkWYalrWl6CE2Votm2Hb4gkgjvlBs4itOYahkSOBUrS157ie+LpOoOD4ekZTqFo6QwMcZDHCaprmi6yA2najrOlaTxBmCXo+rsALHIG7qgh8IYSlQEZxjGcmJrWvE3iAXAphK+GEbEKmkRA5GCJRzDEtRKi0Zo9GMcx+hmhavGcfaTrsW6HoCVAFjejyIliS5kmhjJkbRrG0aKepgqaTS2kkYIZEUVRNEVOkwiMeBCgQGcgRPi+vSnk2r4oAABjpYD5cgmgSKovY7nu3gvsgXEOi4r71vWDpQHuJJLm0bWclA7LxrGpWCJgDbwf4PRNnuGEjiQyBSpwE44DIOi4VZZBAA

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants