Is There Currently Anyway to Concatenate Two or More String Literal Types to a Single String Literal Type in Typescript Right Now

Is there currently anyway to concatenate two or more string literal types to a single string literal type in TypeScript right now?

TS4.1+ answer:

You can now use template literal types to do this:

function makeKey<NS extends string, N extends string>(namespace: NS, name: N) {
return namespace + '/' + name as `${NS}/${N}`
}

const objKey = makeKey('admin', 'home');
// const objKey: "admin/home"

Playground link


Pre TS4.1 answer:

The answer is unfortunately no. There are several feature suggestions filed in GitHub that, if implemented, might give you such functionality (microsoft/TypeScript#12754 to augment keys during mapped types, or microsoft/TypeScript#6579 to manipulate string types via regular expressions) but I don't think they are being actively worked on. I don't see anything in the roadmap about it, anyway. If you really want to see this happen, you might want to go to one of those GitHub issues and give them a or describe your use case if it's particularly compelling. But I wouldn't hold my breath. Sorry!

Typescript - string concatenation as type

In TypeScript v.4.1, (which looks like it will be released in less than 2 weeks time) you can use Template Literal Types to do this really easily:

type Strings = "Fork" | "Spoon";

type FuncNames = `get${Strings}`;

type GetMethods = { [K in FuncNames]: Function }

Playground link

Prior to TS4.1, it doesn't look like this is possible.

Template Literal Types not working with string concatenation

This isn't really a bug, although it might be surprising. It's definitely a concatenation issue.


Concatenation at the type level only happens with template literal types, and only sometimes

First: concatenation with + is not performed at the type level. "foo"+"bar" is of type string in TypeScript, not of type "foobar". There was a suggestion a while ago at microsoft/TypeScript#12940 to support concatenation with +, and people ask about it from time to time, but it was never implemented.

Second: concatenation with template literal strings is only sometimes performed at the type level. Depending on where it's used, `${"foo"}${"bar"}` will either be string or "foobar". You can get concatenation at the type level with a const assertion (as implemented in microsoft/TypeScript#40707), or if the template literal string is used in a place that contextually expects the concatenated version (as implemented in microsoft/TypeScript#43376).

So that gives us the following behavior:

const plus = "foo" + "bar" // string
const tl0 = `${"foo"}${"bar"}` // string
const tl1 = `${"foo"}${"bar"}` as const // "foobar"
const tl2: "foobar" = `${"foo"}${"bar"}` // also "foobar"

Excess property checking does not happen with computed keys of type string

TypeScript has structural subtyping, which means that a value v is assignable to a type T if v contains all the members of T. It doesn't matter if v has more members:

function f(x: { a: string }) {
console.log(x.a.toUpperCase());
}
const val = { a: "hello", b: 123 };
f(val); // okay

From the type system's perspective, excess properties are just fine.

But, there's also excess property checking. When you use an object literal directly in a place that expects fewer properties, it's considered a potential bug because you are throwing away information:

f({ a: "hello", b: 123 }); // error!
// -----------> ~~~~~~
// Object literal may only specify known properties

But, excess property checking does not trigger for computed properties of type string. See microsoft/TypeScript#36920; it seems (although nobody has said so explicitly) that this is not a bug, and asking for it to be changed is considered a feature request. (Also see this comment on microsoft/TypeScript#42686). So for now, anyway, we have the following behavior:

f({ a: "hello", ["B".toLowerCase()]: 123 }) // okay

Lack of concatenation at the type level + no excess property checking on string computed keys leads to the behavior you see here. But I can see why the following might be surprising:

const o: Props = {
[`prop${myNumber}`]: true, // works as expected ✔️
[`prop${myString}`]: true, // does not display error ❌
}

The first one doesn't give an error, but for the same reason that the second one doesn't give an error; they are both considered string-valued keys, so neither are considered excess properties. So that first ✔️ is kind of an accident.

If you want the compiler to treat them as template literal types, you can use const assertions:

const p: Props = {
[`prop${myNumber}` as const]: true, // works as expected ✔️
[`prop${myString}` as const]: true, // error as expected ✔️
}

and now both ✔️ are legitimate.

Playground link to code

Typescript: Is there a union of string literal types for the output of typeof?

There has to be a better way to do this, seeing as those definitions already exist somewhere. But we can access the union in a very roundabout way by:

  • creating a variable with type any
  • calling JS typeof on that variable, which will return the union of all possible types
  • creating a typescript type that uses TS typeof to access that union as a type.
const x: any = "";

const t = typeof x;

type TypeofTypes = typeof t;

This creates

type TypeofTypes = "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"

Typescript Playground Link

Splitting an interface apart and putting it back together again

I'd say the biggest problem with your original version is that there isn't a good inference site for the X type parameter. In

declare function foo<X extends C>(id: X["id"], allButId: Omit<X, "id">): X;

you are hoping that you can pass in a value of type X["id"] and the compiler would infer X from that. But that doesn't happen; the compiler doesn't infer from indexed access types this way; at one point there was a pull request at microsoft/TypeScript#20126 that would have done this, but it never made it into the language. So no matter what you pass in as the id parameter, the compiler will fail to infer anything useful for X and it will fall back to the constraint which is C.


If you want to fix this, the best way to get the compiler to infer a type parameter X is to give it a value of type X from which to infer it. So let's change the type parameter to K extends C["id"] and have id be of type K. Then we have a good chance of inferring either "anA" or "aB" for K:

declare function foo<K extends C["id"]>( id: K, ...

But now we need to express allButId in terms of K. How can we represent the original X in terms of K? For that I'd use the Extract<T, U> utility type to filter a union T to those members assignable to U. In this case X would be Extract<C, {id: K}>. And from that we can Omit the "id" property:

declare function foo<K extends C["id"]>( 
id: K,
allButId: Omit<Extract<C, { id: K }>, "id">
) ...

And what's the return type? Well, let's see what the compiler does infer for {id, ...allButId}:

function foo<K extends C["id"]>( 
id: K,
allButId: Omit<Extract<C, { id: K }>, "id">
) {
return { id, ...allButId };
}

/* function foo<K extends "anA" | "aB">(
id: K, allButId: Omit<Extract<C, { id: K; }>, "id">
): { id: K; } & Omit<Extract<C, { id: K;}>> */

So it infers the intersection type {id: K} & Omit<Extract<C, {id: K}>>. Ideally it would be nice if we could annotate the return type as Extract<C, {id: K}>; after all, shouldn't those be equivalent? Well, they probably are, but the compiler doesn't "understand" that:

function foo<K extends C["id"]>(
id: K,
allButId: Omit<Extract<C, { id: K }>, "id">
): Extract<C, { id: K }> {
return { id, ...allButId }; //error!
}

The idea that splitting a generic type apart and putting it back together should give you the original type is not a concept the compiler understands. It could verify such an equivalent for a specific type, but for a generic like Extract<C, {id: K}>, it isn't up to the task. This is currently a design limitation in TypeScript; there's an open issue at microsoft/TypeScript#28884 asking for support for this, but for now, it doesn't exist.


So what should we do? Well, it's possible that you don't actually need the compiler to verify this. If we just don't annotate foo()'s return type and let the compiler infer that intersection, then maybe that's good enough, as long as it can verify the equivalence of, say, B and {id: "aB"} & Omit<B, "id">:

const b: B = foo("aB", { payload: 123 }); // okay

foo("aB", { payload: "oops" }) ; // error!
// -------> ~~~~~~~ string is not number

const a: A = foo("anA", { payload: "hello" }); // okay

The compiler is happy for you to annotate b as B and a as A. And the compiler complains if you try to call foo() with mismatched parameters. So this all looks good.

Playground link to code

In Typescript, is there a way to declare literal type other than giving `as const` assertion for a specific value?

Please let me know if this is what you are looking for:


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;

/**
* Get union of all values ov the object
*/
type Values<T> = T[keyof T]

/**
* Iterate through the Dictionary - in our case KeyMap
*
*/
type Rename<Obj, Dictionary> =
/**
* Check if Dictionary is a Map structure
*/
Dictionary extends Record<string, string>
/**
* Get all created key/values pair and merge them
* Hence, we only using newly created key/values pairs.
* Obj is not returned from this util type
*/
? UnionToIntersection<Values<{
[Prop in keyof Dictionary]: Prop extends keyof Obj
/**
* Create new key/value pair
*/
? Record<Dictionary[Prop], Obj[Prop]>
: never
}>>
: never

{
// { customAge: 42; }
type Test = Rename<{ age: 42 }, { age: 'customAge' }>

// unknown - I don't know how you want to handle it
type Test2 = Rename<{ age: 42 }, { notExists: 'customAge' }>

// never - because second argument is not a dictionary
type Test3 = Rename<{ age: 42 }, 42>
}

function mapKeys<
/**
* Apply constraint for all keys of Object
*/
ObjKeys extends PropertyKey,
/**
* Apply constraint for all values of Object
*/
ObjValues extends PropertyKey,
/**
* Infer Object with appropriate Keys and Values
*/
Obj extends Record<ObjKeys, ObjValues>,
/**
* Apply constraint for Key of KeyMap
*/
Keys extends keyof Obj,
/**
* Aplly constraint for KeyMap keys
*/
NewKeys extends PropertyKey,
/**
* Infer KeyMap, Keys should extend Object keys
*/
KeyMap extends Record<Keys, NewKeys>
>(obj: Obj, keyMap: KeyMap): Rename<Obj, KeyMap>
function mapKeys<
ObjKeys extends PropertyKey,
ObjValues extends PropertyKey,
Obj extends Record<ObjKeys, ObjValues>,
NewKeys extends PropertyKey,
KeyMap extends Record<keyof Obj, NewKeys>,
>(obj: Obj, keyMap: KeyMap) {
return (Object.keys(obj) as Array<keyof Obj>)
.reduce((acc, elem) => ({
...acc,
[keyMap[elem]]: obj[elem]
}), {} as Record<PropertyKey, Values<Obj>>)
}

const result = mapKeys({ age: 1 }, { "age": "newAge" })
result.newAge // 1

Playground
If yes - I will provide explanation

Please keep in mind, r[keyMap[k]] mutations are not the best option if you want to make it type safe. TS does not track mutations. See my article



Related Topics



Leave a reply



Submit