microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.4k stars 12.41k forks source link

Infer literal types for string concatenations and similar expressions #13969

Closed donaldpipowitch closed 5 months ago

donaldpipowitch commented 7 years ago

TypeScript Version: 2.1.5

Expected behavior:

const foo = 'world'; // value type: 'world'
const bar = `hello ${foo}`; // value type: 'hello world'

Actual behavior:

const foo = 'world'; // value type: 'world'
const bar = `hello ${foo}`; // type: 'string'

Motivation

For constants which are created from other constants the IntelliSense is often more useful, when we see its value type instead of the "real" type.

More practical example:

const fooBreakpoint = '800px';
const fooMediaQuery = `@media(min-width: ${fooBreakpoint})`;

When fooMediaQuery is used I'd like to see @media(min-width: 800px) as its type instead of string.

DanielRosenwasser commented 7 years ago

When fooMediaQuery is used I'd like to see @media(min-width: 800px) as its type instead of string.

This is interesting, mostly because it sounds like you are leveraging your editor's support of string literal types so that you can quickly glance at a type and get its content.

donaldpipowitch commented 7 years ago

Yeah, when I work with CSS-in-JS solutions like glamor it is so great to immediately see some concrete value for different constants instead of just number or string.

import { css } from 'glamor';

const color = '#fff';
const mq = '@media(min-width: 800px)';

const myStyle = css({
  [mq]: {  // hover over `mq` → see '@media(min-width: 800px)'
    color  // hover over `color` → see '#fff'
  }
});

If infering the value type doesn't work (e.g. because of templated strings) I need to duplicate the information into a comment which of course easily gets out of date, if someone changes the values without changing the comment.

import { css } from 'glamor';

const color = '#fff';
const breakpoint = '800px';
/**
 * The media query is '@media(min-width: 800px)'.
 */
const mq = `@media(min-width: ${breakpoint})`;

const myStyle = css({
  [mq]: {  // hover over `mq` → see 'string' as its type and the comment which shows the value
    color  // hover over `color` → see '#fff'
  }
});
fjmorel commented 6 years ago

I'd like to be able to do something like this, building a key using string literals:

type Obj = Record<"a" | "a1" |  "b" | "b1", string>;

function getKey(useA: boolean, useOne: boolean): keyof Obj {
  // [ts] Type 'string' is not assignable to type '"a" | "a1" | "b" | "b1"'.
  return (useA ? "a" : "b") + (useOne ? "1" : "");
}

Adding a cast after the string concatenation makes the error go away, but doesn't actually check that my key generation is still valid if I change Obj.

ethanresnick commented 5 years ago

Similar to @fjmorel, I'd love to have this distribute over a union. My use case is basically:

// i don't control the shape of x.
const x = { item1: true, item2: false, item3: true };

([1, 2, 3] as (1 | 2 | 3)[]).forEach(itemNum => {
    // would love type inference here as item1 | item2 | item3
    const itemKey = `item${itemNum}`;
});
ascott18 commented 5 years ago

This would be fantastic to see for indexing into objects with string literals, especially now that const assertions have been added in 3.4.

const obj= {
 sourceName: "source",
 sourceId: 1,
 targetName: "target",
 targetId: 2,
}

function getValues(prefix: "source" | "target") {
 return { name: obj[prefix + "Name"], id: obj[prefix + "Id"] }
}
vdh commented 4 years ago

It'd be nice to be able to use either string literals or unions.

const bar = "bar";
const foobar = `foo${bar}`; // "foobar"
let union: "bar" | "baz";
const foobarOrFoobaz = `foo${union}`; // "foobar" | "foobaz"
let generic: string;
const stillGeneric = `foo${generic}`; // string
mortoray commented 4 years ago

This would be helpful for string enums with a common prefix.

const prefix = "mymodule_"
export enum Actions {
  actionOne: prefix + "action_one"
  actionTwo: prefix + "action_two"
}

This is currently rejected with error TS2553: Computed values are not permitted in an enum with string valued members.

vdh commented 4 years ago

Maybe a const assertion syntax would suit this?

const bar = "bar"; // "bar"
const existingBehaviour = `foo-${bar}`; // string
const dynamic = process.env.BAR; // string | undefined
const existingBehaviourDynamic = `foo-${dynamic}`; // string
const barOrBaz = Math.round(Math.random()) ? "bar" : "baz"; // "bar" | "baz"
// A const assertion syntax…?
const fooBar = `foo-${bar}` as const; // "foo-bar"
const fooBarOrBaz = `foo-${barOrBaz}` as const; // "foo-bar" | "foo-baz"
const badDynamicVariable = `foo-${dynamic}` as const; // type error
jcalz commented 4 years ago

this is not inferred yet, but now we at least have #40336 to represent string concatenation at the type level,

bradzacher commented 1 year ago

Worth noting that if you add as const this should work now with template literal types!

https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZiEMC8MDkB3EAnANgEzQG4YB6UmANwENcBXAUxigE8AHBgLnSz0IChQkWACNq2FDAAGACwa5cSACQBvBCAC+UmNQgwh0Iv3IxTpgHoB+IA

jakebailey commented 5 months ago

This was fixed in #53907, which I believe has enough testing for this.