I'm trying to build a typescript library for learning purpose, For a better developer experience I want typescript to infer the string literal type from one of the argument passed in function which is an array of objects.
For example: Let say I've a fruit array object, an object that contains name property which can store a string value.
const fruitArray = [
{ name: "apple" },
{ name: "orange" },
{ name: "banana" }
]
type FruitArray = typeof fruitArray
Now I've a function that takes this array object as first argument, and second argument that takes one of the fruit.name.
function getFruitName<T extends FruitArray>(fruitArray: T, name:T[number]["name"]) { /** ... **/ }
When I invoke this function with required arguments it shows typeof name is string in getFruitName function.
getFruitName(fruitArray, "apple")
the name argument can only be one of the values present in name property of fruit object. I want typescript to infer that string to string literal of those values,
// Typescript should infer type of name like this instead of string
type name = "apple" | "orange" | "banana"
I can achieve this by making the fruitArray as const.
typescript playground link
const fruitArray = [
{ name: "apple" },
{ name: "orange" },
{ name: "banana" }
] as const;
I don't want to do this. The reason for this is that If I declare as const I lose the type for the array objects while writing and there are more properties other than name.
The developer experience I want using this function is that, user put in the first argument which contains the array of objects with name property in it, and then when they try to add second argument the only available options would be one of the values in array object name property.
I don't even know whether it is possible or not. Any suggestion is welcome.
Update:
One of the issue that I faced using as const is that I don't have type safety while writing the fruitArray, that can be solved by using satisfies operator. As @jcalz mentioned in this typescript playground link
But I would prefer to handle the typing of name in string literals on library side itself, that would be perfect since it won't require user to do extra work to get the type safety in name argument.
// Library Code
type Fruit = {name:string, age:number}
function getFruitName<T extends Fruit[]>(fruitArray: T, name:T[number]["name"]) { /** ... **/ }
// using library
const fruitArray:Fruit[] = [
{ name: "apple", age: 1 },
{ name: "orange", age: 2 },
{ name: "banana", age: 3 }
]
getFruitName(fruitArray, "") // should infer string literals from fruit Array
We can make any changes to library code itself.
Unfortunately what you are asking for is impossible. Once you've written this code
it is too late for the compiler to help you. Since
fruitArrayis annotated as being of typeFruit[], that's all the compiler knows aboutfruitArray. (IfFruit[]were a a union type, then the compiler could narrowfruitArrayupon assignment to just those union members compatible with the assigned value. But it's not, so no such narrowing happens.)Essentially, the type annotation throws away any more specific information the compiler may have had about the value you assigned to the variable. If you want to preserve such information you cannot annotate the type, and you will even want to use a
constassertion so that the compiler infers something more specific than{name: string, age: number}[]. And if you care about making sure the value conforms to the type you wanted to annotate, then you should use thesatisfiesoperator to do so.Indeed, for some literal expression
{...}, the assignmentconst x: T = {...}tells the compiler that you don't particularly care about that literal expression, whileconst x = {...} as const satisfies Ttells the compiler that you do care. Your library users essentially have no choice but to do the latter instead of the former.The only possible suggestion I could make is for your library function to detect that the user has failed to declare
fruitArrayproperly, by complaining unless thenameproperty of its elements are literal types instead of juststring. Here's one way to do that:The idea is that
StringLiteral<T["name"]>will evaluate toT["name"]if and only ifT["name"]is a string literal type (well, if it's narrower thanstring). Otherwise it will evaluate toInvalid<"...">, an incompatible type containing a message the user can hopefully understand (this is a workaround for the lack of custom compiler errors in TypeScript, as requested in microsoft/TypeScript#23689).So if you call it incorrectly, you get an error:
which hopefully prompts the user to go back and fix their mistake:
It's not perfect, but at least users will be alerted that something has gone wrong.
Playground link to code