I'm trying to write a function which will take a string literal and return an object with a single field whose name is that string literal. I can write a function that does what I want, but I don't know how to express the constraint that its argument type be a string literal.
The closest I've got is using a generic type which extends string
. This permits string literal types, but also unions of string literal types and the type string
, which I don't want to be passed to my function.
This compiles, and does what I want, provided that K
is a string literal type. Note that type assertion wasn't necessary in typescript 3.4 but it is required in 3.5.
function makeObject<K extends string>(key: K): { [P in K]: string } {
return { [key]: "Hello, World!" } as { [P in K]: string };
}
If K
is anything other than a string literal, the return type of this function won't be the same as the type of the value it returns.
The 2 avenues I can imagine for making this work are:
Can typescript's type system express either of these?
If I remove the type assertion in typescript 3.5 I get the error:
a.ts:2:3 - error TS2322: Type '{ [x: string]: string; }' is not assignable to type '{ [P in K]: string; }'.
2 return { [key]: "Hello, World!" };
Update for TypeScript 4.2
The following works:
type StringLiteral<T> = T extends string ? string extends T ? never : T : never;
(No longer works): TypeScript 4.1 template literal type trick
Edit: The below actually broke in 4.2. Discussion here
type StringLiteral<T> = T extends `${string & T}` ? T : never;
TS 4.1 introduced template literal types which allows you to convert string literals to other string literals. You can convert the string literal to itself. Since only literals can be templated and not general strings, you just then conditionally check that the string literal extends from itself.
Full example:
type StringLiteral<T> = T extends `${string & T}` ? T : never;
type CheckLiteral = StringLiteral<'foo'>; // type is 'foo'
type CheckString = StringLiteral<string>; // type is never
function makeObject<K>(key: StringLiteral<K>) {
return { [key]: 'Hello, World!' } as { [P in StringLiteral<K>]: string };
}
const resultWithLiteral = makeObject('hello'); // type is {hello: string;}
let someString = 'prop';
const resultWithString = makeObject(someString); // compiler error.
I don't think unions for K
are a problem any more because there is no need to narrow the type for the property key in the makeObject
signature. If anything this becomes more flexible.
There is no constraint for something to be a single string literal type. If you specify extends string
the compiler will infer string literal types for K
but it will also by definition allow unions of string literal types (after all the set of a union of string literal types is included in the set of all strings)
We can create a custom error, that forces as call to be in an error state if it detects a union of string literal types. Such a check can be done using conditional types making sure that K
is the same as UnionToIntersection<K>
. If this is true K
is not a union, since 'a' extends 'a'
but 'a' | 'b'
does not extends 'a' & 'b'
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type CheckForUnion<T, TErr, TOk> = [T] extends [UnionToIntersection<T>] ? TOk : TErr
function makeObject<K extends string>(key: K & CheckForUnion<K, never, {}>): { [P in K]: string } {
return { [key]: "Hello, World!" } as { [P in K]: string };
}
makeObject("a")
makeObject("a" as "a" | "b") // Argument of type '"a" | "b"' is not assignable to parameter of type 'never'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With