英文:
How can I ensure that the parameter passed to a function is of a same type as the parameter of a previous function?
问题
我想调用一个名为 createLoadBalancer(name: string)
的函数,然后调用另一个名为 attachInstanceToLb(lbName: string)
的函数,同时在编译时确保第二个调用中的 lbName
已在第一个调用中使用。
我的最初的、天真的想法是这样做的:
type LoadBalancerName = "first" | "second";
const createLoadBalancer(name: LoadBalancerName) = () => { ... }
const attachInstanceToLb(lbName: LoadBalancerName) = () => { ... }
但这个解决方案的问题是,我仍然可以调用:
createLoadBalancer('first');
attachInstanceToLb('second');
而 TypeScript 不会抱怨。
我如何确保只能使用调用 createLoadBalancer()
时作为参数的负载均衡器名称来调用 attachInstanceToLb
函数?
英文:
I would like to call a function called createLoadBalancer(name: string)
, and then call another function called attachInstanceToLb(lbName: string)
while ensuring at compile time that the lbName
of the second call has been used in the first call.
My initial, naive thought, was to do the following:
type LoadBalancerName = "first" | "second";
const createLoadBalancer(name: LoadBalancerName) = () => { ... }
const attachInstanceToLb(lbName: LoadBalancerName) = () => { ... }
but the issue with that solution is that I can still call
createLoadBalancer('first');
attachInstanceToLb('second');
and TypeScript won't complain.
How can I ensure that the attachInstanceToLb
function can only be called with load balancer names that have been used as a parameter when calling createLoadBalancer()
?
答案1
得分: 0
TypeScript 的类型系统不会建模任意状态的变化,它根本不会建模全局状态的变化。一般来说,它将值视为具有类型,而这些类型不会真正改变。嗯,编译器有一些能力可以在某个范围内narrow值的表现类型,但它们不会追踪任意更改。没有办法告诉 TypeScript:
createLoadBalancer('first');
attachInstanceToLb('second'); // 错误
createLoadBalancer('second');
attachInstanceToLb('first');
是错误的,但是这个
createLoadBalancer('first');
attachInstanceToLb('first');
createLoadBalancer('second');
attachInstanceToLb('second');
是正确的,因为这将涉及到函数调用具有某种全局副作用,而 TypeScript 无法跟踪到这一点。
最接近的方法可能是将您的函数更改为assertion functions,它们可以只缩小一个参数的表现类型。这意味着您必须添加一个参数来跟踪状态,并为该参数赋予一种在每次调用后变得更窄的类型。这里是一种可能性:
interface State extends Partial<Record<LoadBalancerName, true>> {}
const state: State = {};
function createLoadBalancer<K extends LoadBalancerName>(
state: State, name: K): asserts state is Record<K, true> {
state[name] = true;
}
function attachInstanceToLb<T extends State>(
state: T, name: keyof T) {
};
因此,当您调用 createLoadBalancer(state, val)
时,state
的类型将缩小为具有键为 val
的属性且值为 true
的对象:
// const state: State
createLoadBalancer(state, "first");
// const state: Record<"first", true>
attachInstanceToLb(state, "first");
attachInstanceToLb(state, "second"); // 错误
createLoadBalancer(state, "second");
// const state: Record<"first", true> & Record<"second", true>
attachInstanceToLb(state, "second"); // 可行
这种方法有一些局限性。如果您在一个范围内执行某些调用,而在另一个范围内执行其余调用,编译器可能无法看到它们以必要的顺序发生。如果您发现自己需要表示更多状态(例如,在您执行 attachInstanceToLb
后,不能为相同的键再次执行它),您最终可能无法将该状态更改表示为缩小。
在 TypeScript 中执行此类操作的“正确”方法是考虑从将状态更改表示为副作用转向将状态表示为不同的值。这意味着:每次调用 createLoadBalancer()
都会返回一个知道特定键的新对象,因此甚至无法表示不正确的调用:
function createLoadBalancer(name: LoadBalancerName) {
return {
attachInstance() {}
}
}
const lbFirst = createLoadBalancer("first");
lbFirst.attachInstance();
const lbSecond = createLoadBalancer("second");
lbSecond.attachInstance();
这里的 lbFirst
知道 first
,并且没有办法让它 attachInstance("second")
。lbSecond
也一样。您可能无法以这种方式重构,但它的行为将比任意状态更改或断言函数好得多。
英文:
TypeScript's type system doesn't model arbitrary state changes, and it doesn't model global state changes at all. Generally speaking it sees values as having types, and these types don't really change. Well, there is some ability for the compiler to narrow the apparent type of a value in some scope, but these don't track arbitrary changes. There's no way to tell TypeScript that
createLoadBalancer('first');
attachInstanceToLb('second'); // error
createLoadBalancer('second');
attachInstanceToLb('first');
is bad but that
createLoadBalancer('first');
attachInstanceToLb('first');
createLoadBalancer('second');
attachInstanceToLb('second');
is good, because it would involve having function calls have some sort of global side-effects and TypeScript can't track that.
The closest I can imagine getting to this would be to change your functions into assertion functions which can narrow (and only narrow) the apparent type of one of their parameters. So that would mean you'd have to add a parameter to keep track of the state, and give that parameter a type which gets narrower after each call. Here's one possibility:
interface State extends Partial<Record<LoadBalancerName, true>> {}
const state: State = {};
function createLoadBalancer<K extends LoadBalancerName>(
state: State, name: K): asserts state is Record<K, true> {
state[name] = true;
}
function attachInstanceToLb<T extends State>(
state: T, name: keyof T) {
};
So when you call createLoadBalancer(state, val)
, the type of state
is narrowed to something where the property with key at val
has a true
value:
// const state: State
createLoadBalancer(state, "first");
// const state: Record<"first", true>
attachInstanceToLb(state, "first");
attachInstanceToLb(state, "second"); // error
createLoadBalancer(state, "second");
// const state: Record<"first", true> & Record<"second", true>
attachInstanceToLb(state, "second"); // okay
That sort of works, but it's fragile. If you do some of the calls in one scope and the rest in another, the compiler might fail to see that they happen in the necessary order. And if you find yourselves needing to represent more state, (e.g., after you attachInstanceToLb
you can't do it a second time for the same key) you might eventually find yourself unable to represent that state change as a narrowing.
The "right" way to do things like this in TypeScript is to think about refactoring away from representing state changes as side-effects and toward representing state as different values. That means: each call to createLoadBalancer()
returns a new object which knows about the specific key, so it's not possible to even represent an incorrect call:
function createLoadBalancer(name: LoadBalancerName) {
return {
attachInstance() { }
}
}
const lbFirst = createLoadBalancer("first");
lbFirst.attachInstance();
const lbSecond = createLoadBalancer("second");
lbSecond.attachInstance();
Here lbFirst
knows about "first"
and there's no way to have it attachInstance("second")
. And the same with lbSecond
. You might not be able to refactor this way, but it will behave much better than arbitrary state changes or assertion functions.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论