Type safety with `TemplateStringsArray` and tag functions

未关闭
#33,304 22 条评论 213 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
25/100
Issue 类型
功能
描述清晰度
需要澄清
活跃度
停滞
技术栈
typescript
领域
compilers

调研方向

检查 TypeScript/src/lib/es5.d.ts 和 TypeScript/lib/lib.es5.d.ts 中的 TemplateStringsArray 声明,然后复现所提供的 selectTranslation 示例中的标签调用。确定是否可以在不破坏现有类型的情况下指定所请求的泛型数组和类型安全的标签函数行为;完成标准是有一个获得接受的设计以及相应的类型检查覆盖。

由索引模型根据 Issue 内容生成。

描述

In Discussion Suggestion

Search Terms

TemplateStringsArray, type, safety, generic, tag, function

Suggestion

Hello, I'd like to know how could I achieve type safety with TemplateStringsArray and tag functions or make such scenarios possible.

Use Cases

I would use TemplateStringsArray together with the

tagFunction`<type_safe_accessor>`

Currently, there's no way to do this, since

  1. the definition of TemplateStringsArray is not generic
// source code:
// TypeScript/src/lib/es5.d.ts
// TypeScript/lib/lib.es5.d.ts

// local install:
// /usr/lib/code/extensions/node_modules/typescript/lib/lib.es5.d.ts

interface TemplateStringsArray extends ReadonlyArray<string> {
    readonly raw: ReadonlyArray<string>;
}

AND

  1. the usage of
tagFunction`<type_safe_accessor>`

gives the following error, completely preventing the type-safe usage:

Argument of type '{}' is not assignable to parameter of type keyof TypeAndValueObject | TemplateStringsArray<keyof TypeAndValueObject>'.
  Type '{}' is missing the following properties from type 'TemplateStringsArray<keyof TypeAndValueObject>': raw, length, concat, join, and 19 more. ts(2345)

Examples

The working case

I have some type-safe i18n translations:

// Dictionary.ts
export interface Dictionary {
	"Hello": string;
	"Click to see more": string;
	"Today is": (day: string) => string;
}

// en.ts
import { Dictionary } from "./Dictionary";

export const en: Dictionary = {
	"Hello": "Hello",
	"Click to see more": "Click to see more",
	"Today is": (day: string) => `Today is ${day}`,
};

// lt.ts
import { Dictionary } from "./Dictionary";

export const lt: Dictionary = {
	"Hello": "Sveiki",
	"Click to see more": "Paspauskite, kad pamatytumėte daugiau",
	"Today is": (day: string) => `Šiandien yra ${day}`,
};
// i18n.ts
import { Dictionary } from "./Dictionary";
import { en } from "./en";
import { lt } from "./lt";

export interface ITranslations {
	en: Dictionary;
	lt: Dictionary;
}

export const translations: ITranslations = {
	en: en,
	lt: lt,
};

// "en" | "lt"
export type ILang = keyof ITranslations;

I have a function to get the translations:

import { ILang, translations } from "./i18n";
import { Dictionary } from "./Dictionary";

const currentLang: ILang = "en";

const dictionary: Dictionary = translations[currentLang];

export const selectTranslation = <K extends keyof Dictionary>(key: K): Dictionary[K] => {
	const translationText: Dictionary[K] = dictionary[key];
	return translationText;
};

And I can safely use it with type-safe translations in the following fashion:

// someFile.ts
import { selectTranslation } from "../selectTranslation.ts";

/**
 * Type-checks, auto-completes etc. the strings from the `Dictionary`
 */
const someText: string = selectTranslation("Hello");
The problem

However, now I'd like to use the template string (template literal) syntax, paired with the tag function to achieve the same functionality, so I improve the selectTranslation function:

// selectTranslation.ts
import { ILang, translations } from "./i18n";
import { Dictionary } from "./Dictionary";

const currentLang: ILang = "en";

const dictionary: Dictionary = translations[currentLang];

export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringArray): Dictionary[K] => {
	let realKey: K;

	if (Array.isArray(key)) {
		realKey = key[0];
	} else {
		realKey = key; // error:
		/** 
		 * Type 'K | TemplateStringsArray<string>' is not assignable to type 'K'.
  		 * Type 'TemplateStringsArray<string>' is not assignable to type 'K'. ts(2322)
		*/
	}

	const translationText: Dictionary[K] = dictionary[realKey];
	return translationText;
};
// anotherFile.ts
import { selectTranslation } from "../selectTranslation.ts";

/**
 * Does NOT type-check and gives the previously mentioned error:
 * 
 * Argument of type '{}' is not assignable to parameter of type K | TemplateStringsArray<K>'.
 * Type '{}' is missing the following properties from type 'TemplateStringsArray<K>': raw, length, concat, join, and 19 more. ts(2345)
 */
const someText: string = selectTranslation`Hello`; // error

Possible solutions

I've tried 3 different solutions, neither of them giving me the results that I want:

a) Change the TemplateStringsArray to be a generic like so
// source code:
// TypeScript/src/lib/es5.d.ts
// TypeScript/lib/lib.es5.d.ts

// local install:
// /usr/lib/code/extensions/node_modules/typescript/lib/lib.es5.d.ts

interface TemplateStringsArray<T = string> extends ReadonlyArray<T> {
    readonly raw: ReadonlyArray<T>;
}

and used it like so

// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray<K>): Dictionary[K] => {

however, the same problems persisted - the casting from key to realKey failed
AND the tagged function usage still failed

// selectTranslation.ts

if (Array.isArray(key)) {
	realKey = key[0];
} else {
	realKey = key; // still errors
}

// anotherFile.ts
const someText: string = selectTranslation`Hello`; // still errors
b) Instead of using TemplateStringsArray, just use Array<K>
// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | Array<K>): Dictionary[K] => {

the first problem of casting from key to realKey disappeared,
BUT the second one still remained

// selectTranslation.ts

if (Array.isArray(key)) {
	realKey = key[0];
} else {
	realKey = key; // all cool now
}

// anotherFile.ts
const someText: string = selectTranslation`Hello`; // still errors!
c) Just use any
// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | any): Dictionary[K] => {

which then allows me to use the tag function, BUT there's NO type-safety, autocompletions etc., making it practically useless.

TL;DR:

Neither of the solutions helped - I'm still unable to use the selectTranslation function as a tag function with type safety.

Is there any way to make it possible?

Reminder - we have 2 problems here:

  1. (less important since it can be avoided by using Array<K>) TemplateStringsArray does not work like it should (or I'm using it wrong), even when used as a generic
  2. (more important) tag functions cannot have type-safe parameters?

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

I'm happy to help if you have any further questions.

主要语言
Go
星标
111k
派生
14.4k
平均合并
1 天 19 小时
30 天内合并 PR
117

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/TypeScript 的其他 Issue

查看 microsoft/TypeScript 的全部 Issue

相似的 Issue

更多 Go Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。