18

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:

  • constrain K to be only string literals
  • express that the return type be an object with a single field whose name is a value in K (less satisfying, but at least the type of the function will be honest)

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!" };
stevebob
  • 183
  • 1
  • 1
  • 4
  • Not sure what the result should be though. what should `makeObject("a" as "a" | "b");` return ? `{ a?: string; b?: string; }` maybe ? we can get an trigger an error if `K` is not a single string literal (ie not a union) is this what you want ? – Titian Cernicova-Dragomir May 30 '19 at 07:48
  • Ideally your example would result in a type error because the argument is a union of string literals instead of a string literal. – stevebob May 30 '19 at 09:02

3 Answers3

16

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.

Andy K
  • 161
  • 1
  • 5
9

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'
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • What is the purpose of writing tuple types `[T] extends [UnionToIntersection]`? Is it just to prevent the condition from being distributed over the union in the case where `T` is a union type? – stevebob May 30 '19 at 22:34
  • Also, for an explanation of why `UnionToIntersection` works, see https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286 – stevebob May 30 '19 at 22:38
0

Not sure if this was a recent change, but now it seems a type assertion as { [L in K]: V } is enough to do the trick. No type tricks necessary.

function foo<Key extends string, Value extends number>(key: Key, value: Value) {
  return {
    [key]: value
  } as { [K in Key]: Value }
}

const bar = foo("age", 42);
// { age: 42; }
mdcq
  • 1,593
  • 13
  • 30